使用 PyTorch C ++前端
使用 PyTorch C ++前端
PyTorch C ++前端是 PyTorch 机器学习框架的纯 C ++接口。 虽然 PyTorch 的主要接口自然是 Python,但此 Python API 建立于大量的 C ++代码库之上,提供基本的数据结构和功能,例如张量和自动微分。 C ++前端公开了纯 C ++ 11 API,该 API 使用机器学习训练和推理所需的工具,扩展了此基础 C ++代码库。 该拓展包括用于神经网络建模的通用组件的内置集合; 使用自定义模块扩展此集合的 API; 一个流行的优化算法库,例如随机梯度下降; 具有 API 的并行数据加载器,用于定义和加载数据集; 序列化例程等。
本教程将引导您完成使用 C ++前端训练模型的端到端示例。 具体来说,我们将训练 DCGAN (一种生成模型),以生成 MNIST 数字的图像。 虽然从概念上讲,这只是一个简单的示例,但它足以使您对 PyTorch C ++前端有个大概的了解,并可以满足训练更复杂模型的需求。 我们将从一些鼓舞人心的词开始,说明您为什么要使用 C ++前端,然后直接深入定义和训练我们的模型。
Tip
观看来自 CppCon 2018 的简短的演讲,获得有关 C ++前端的快速而又幽默的演示。
Tip
本笔记概述了 C ++前端的组件和设计原理。
Tip
有关 PyTorch C ++生态系统的文档,请访问 https://pytorch.org/cppdocs 。 您可以在此处找到高级描述以及 API 级文档。
动机
在我们开始 GAN 和 MNIST 数字的激动人心的旅程之前,让我们退一步来讨论为什么您要使用 C ++前端而不是 Python。 我们(PyTorch 团队)创建了 C ++前端,以便能够在无法使用 Python 或根本不适合该工具的环境中进行研究。 此类环境的示例包括:
- 低延迟系统:您可能希望在具有高帧率和低延迟要求的纯 C ++游戏引擎中进行强化学习研究。 与 Python 库相比,使用纯 C ++库更适合这种环境。 由于 Python 解释器运行缓慢,Python 可能根本无法处理此类问题。
- 高度多线程环境:由于全局解释器锁定(GIL),Python 一次不能运行多个系统线程。 并行处理是一种替代方法,但可扩展性不强,并且存在很多缺点。 C ++没有这样的约束,线程易于使用和创建。 需要高度并行化的模型,例如深层神经进化中使用的模型,可以从中受益。
- 现有的 C ++代码库:您可能下载了 C ++应用程序,其工作范围从后端服务器中的网页服务到照片编辑软件中的 3D 图形渲染应有尽有,并且希望将机器学习方法集成到您的系统中。 C ++前端使您可以继续使用 C ++,并省去在 Python 和 C ++之间来回绑定的麻烦,同时保留了传统 PyTorch(Python)大部分的灵活性和直观性。
C ++前端与 Python 前端并非是竞争关系。 前者是对后者的补充。 我们知道研究人员和工程师都喜欢 PyTorch,因为它具有简单,灵活和直观的 API。 我们的目标是确保您可以在所有可能的环境(包括上述环境)中利用这些核心设计原则。 如果上述的这些情况之一很好地描述了您的用例,或者您只是感兴趣或好奇,请在以下段落中继续研究 C ++前端。
Tip
C ++前端试图提供一个与 Python 前端尽可能接近的 API。 如果您对 Python 前端有丰富的经验,并且问过自己“我可以使用 C ++前端做些什么 ?”,请像在 Python 中那样编写代码,并且大多数情况下,相同的函数和方法都可以在 C ++中使用。 就像在 Python 中一样(记得用双冒号替换点)。
编写基本应用程序
首先,编写一个最小的 C ++应用程序,以验证我们是否在同一页面上了解我们的设置和构建环境。 首先,您需要获取 LibTorch 发行版的副本-我们现成的 zip 归档文件,其中打包了使用 C ++前端所需的所有相关首部,库和 CMake 构建文件。 LibTorch 发行版可在 PyTorch 网站上下载,适用于 Linux,MacOS 和 Windows。 本教程的其余部分将假定基本的 Ubuntu Linux 环境,但是您也可以在 MacOS 或 Windows 上进行学习。
Tip
关于安装 PyTorch的 C ++发行版 的注释更详细地描述了以下步骤。
Tip
在 Windows 上,调试和发行版本不兼容 ABI。 如果您打算以调试模式构建项目,请尝试使用 LibTorch 的调试版本。 另外,请确保在下面的cmake --build .
行中指定正确的配置。
第一步,通过从 PyTorch 网站获取的链接在本地下载 LibTorch 发行版。 对于普通的 Ubuntu Linux 环境,这意味着运行以下步骤:
# If you need e.g. CUDA 9.0 support, please replace "cpu" with "cu90" in the URL below.
wget https://download.pytorch.org/libtorch/nightly/cpu/libtorch-shared-with-deps-latest.zip
unzip libtorch-shared-with-deps-latest.zip
接下来,让我们编写一个名为dcgan.cpp
的小型 C ++文件,其中包含torch/torch.h
,现在只需打印出三乘三的单位矩阵即可:
#include <torch/torch.h>
#include <iostream>int main() {torch::Tensor tensor = torch::eye(3);std::cout << tensor << std::endl;
}
要在以后构建这个应用程序以及我们完整的训练脚本,我们将使用以下CMakeLists.txt
文件:
cmake_minimum_required(VERSION 3.0 FATAL_ERROR)
project(dcgan)find_package(Torch REQUIRED)add_executable(dcgan dcgan.cpp)
target_link_libraries(dcgan "${TORCH_LIBRARIES}")
set_property(TARGET dcgan PROPERTY CXX_STANDARD 14)
注意
虽然 CMake 是 LibTorch 的推荐的构建系统,但这并不是硬性要求。 您还可以使用 Visual Studio 项目文件,QMake,普通 Makefile 或您认为合适的任何其他构建环境。 但是,我们不为此提供现成的支持。
在上面的 CMake 文件中记下第 4 行:find_package(Torch REQUIRED)
。 这表示 CMake 在查找 LibTorch 库的构建配置。 为了使 CMake 知道在哪里找到这些文件,调用cmake
时必须设置CMAKE_PREFIX_PATH
。 在执行此操作之前,让我们就dcgan
应用程序的以下目录结构达成一致:_
dcgan/CMakeLists.txtdcgan.cpp
此外,我将指向未压缩的 LibTorch 分布的路径称为/path/to/libtorch
。 请注意,此必须是绝对路径。 特别是,将CMAKE_PREFIX_PATH
设置为../../libtorch
之类的内容会以意想不到的方式中断, 应该写$PWD/../../libtorch
以获取相应的绝对路径。 现在,我们准备构建我们的应用程序:
root@fa350df05ecf:/home# mkdir build
root@fa350df05ecf:/home# cd build
root@fa350df05ecf:/home/build# cmake -DCMAKE_PREFIX_PATH=/path/to/libtorch ..
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Looking for pthread.h
-- Looking for pthread.h - found
-- Looking for pthread_create
-- Looking for pthread_create - not found
-- Looking for pthread_create in pthreads
-- Looking for pthread_create in pthreads - not found
-- Looking for pthread_create in pthread
-- Looking for pthread_create in pthread - found
-- Found Threads: TRUE
-- Found torch: /path/to/libtorch/lib/libtorch.so
-- Configuring done
-- Generating done
-- Build files have been written to: /home/build
root@fa350df05ecf:/home/build# cmake --build . --config Release
Scanning dependencies of target dcgan
[ 50%] Building CXX object CMakeFiles/dcgan.dir/dcgan.cpp.o
[100%] Linking CXX executable dcgan
[100%] Built target dcgan
上面,我们首先在dcgan
目录内创建一个build
文件夹,进入该文件夹,运行cmake
命令以生成必要的 build(Make)文件,最后通过运行cmake --build . --config Release
成功编译该项目。 现在我们准备执行最小的二进制文件并完成有关基本项目配置的这一部分:
root@fa350df05ecf:/home/build# ./dcgan
1 0 0
0 1 0
0 0 1
[ Variable[CPUFloatType]{3,3} ]
在我看来这就像一个单位矩阵!
定义神经网络模型
现在我们已经配置了基本环境,我们可以深入研究本教程中更有趣的部分。 首先,我们将讨论如何在 C ++前端中定义模块并与之交互。 我们将从基本的小规模示例模块开始,然后使用 C ++前端提供的广泛的内置模块库来实现完整的 GAN。
模块 API 基础
与 Python 界面一致,基于 C ++前端的神经网络由称为_模块_的可重用构建块组成。 有一个基础模块类,所有其他模块都从该基础类派生。 在 Python 中,此类为torch.nn.Module
,在 C ++中为torch::nn::Module
。 除了实现模块封装的算法的forward()
方法之外,模块通常还包含以下三种子对象中的任何一种:参数,缓冲区和子模块。
参数和缓冲区以张量的形式存储。 参数记录梯度,但缓冲区不记录。 参数通常是神经网络的可训练权重。 缓冲区的示例包括批量标准化的均值和方差。 为了重用特定的逻辑和状态块,PyTorch API 允许嵌套模块。 嵌套模块称为_子模块_。
参数,缓冲区和子模块是必须被注册的。 注册后,可以使用parameters()
或buffers()
之类的方法来检索整个(嵌套)模块层次结构中所有参数的容器。 类似地,使用to(...)
之类的方法,例如 to(torch::kCUDA)
将所有参数和缓冲区从 CPU 移到 CUDA 内存,在整个模块层次结构上工作。
定义模块和注册参数
为了将这些词写成代码,让我们考虑一下用 Python 界面编写的简单模块:
import torchclass Net(torch.nn.Module):def __init__(self, N, M):super(Net, self).__init__()self.W = torch.nn.Parameter(torch.randn(N, M))self.b = torch.nn.Parameter(torch.randn(M))def forward(self, input):return torch.addmm(self.b, input, self.W)
在 C ++中,它看起来像这样:
#include <torch/torch.h>struct Net : torch::nn::Module {Net(int64_t N, int64_t M) {W = register_parameter("W", torch::randn({N, M}));b = register_parameter("b", torch::randn(M));}torch::Tensor forward(torch::Tensor input) {return torch::addmm(b, input, W);}torch::Tensor W, b;
};
就像在 Python 中一样,我们定义了一个名为Net
的类(为简单起见,这里是struct
而不是class
),然后从模块基类派生它。 在构造函数内部,我们使用torch::randn
创建张量,就像在 Python 中使用torch.randn
一样。 一个有趣的区别是我们如何注册参数。 在 Python 中,我们用torch.nn.Parameter
类包装了张量,而在 C ++中,我们不得不通过register_parameter
方法传递张量。 这样做的原因是 Python API 可以检测到属性为torch.nn.Parameter
类型并自动注册此类张量。 在 C ++中,反射非常有限,因此提供了一种更传统(而且并不是那么不可思议)的方法。
注册子模块并遍历模块层次结构
同样,我们可以注册参数,也可以注册子模块。 在 Python 中,将子模块分配为模块的属性时,会自动检测并注册这些子模块:
class Net(torch.nn.Module):def __init__(self, N, M):super(Net, self).__init__()# Registered as a submodule behind the scenesself.linear = torch.nn.Linear(N, M)self.another_bias = torch.nn.Parameter(torch.rand(M))def forward(self, input):return self.linear(input) + self.another_bias
例如,允许使用parameters()
方法来递归访问模块层次结构中的所有参数:
>>> net = Net(4, 5)
>>> print(list(net.parameters()))
[Parameter containing:
tensor([0.0808, 0.8613, 0.2017, 0.5206, 0.5353], requires_grad=True), Parameter containing:
tensor([[-0.3740, -0.0976, -0.4786, -0.4928],[-0.1434, 0.4713, 0.1735, -0.3293],[-0.3467, -0.3858, 0.1980, 0.1986],[-0.1975, 0.4278, -0.1831, -0.2709],[ 0.3730, 0.4307, 0.3236, -0.0629]], requires_grad=True), Parameter containing:
tensor([ 0.2038, 0.4638, -0.2023, 0.1230, -0.0516], requires_grad=True)]
要在 C ++中注册子模块,请使用恰当命名的register_module()
方法注册类似torch::nn::Linear
的模块:
struct Net : torch::nn::Module {Net(int64_t N, int64_t M): linear(register_module("linear", torch::nn::Linear(N, M))) {another_bias = register_parameter("b", torch::randn(M));}torch::Tensor forward(torch::Tensor input) {return linear(input) + another_bias;}torch::nn::Linear linear;torch::Tensor another_bias;
};
Tip
您可以在torch::nn
命名空间的文档中找到可用的内置模块的完整列表,例如torch::nn::Linear
,torch::nn::Dropout
或torch::nn::Conv2d
。
微妙之处在于,为什么在构造函数的初始值设定项列表中创建子模块,而在构造函数的主体内部创建参数。 这是有充分的理由的,我们将在下面有关 C ++前端的_所有权模型_的部分中对此进行介绍。 但是,最终结果是,就像 Python 中一样,我们可以递归访问模块树的参数。 调用parameters()
将返回std::vector<torch::Tensor>
,我们可以对其进行迭代:
int main() {Net net(4, 5);for (const auto& p : net.parameters()) {std::cout << p << std::endl;}
}
打印:
root@fa350df05ecf:/home/build# ./dcgan
0.0345
1.4456
-0.6313
-0.3585
-0.4008
[ Variable[CPUFloatType]{5} ]
-0.1647 0.2891 0.0527 -0.0354
0.3084 0.2025 0.0343 0.1824
-0.4630 -0.2862 0.2500 -0.0420
0.3679 -0.1482 -0.0460 0.1967
0.2132 -0.1992 0.4257 0.0739
[ Variable[CPUFloatType]{5,4} ]
0.01 *
3.6861
-10.1166
-45.0333
7.9983
-20.0705
[ Variable[CPUFloatType]{5} ]
具有三个参数,就像在 Python 中一样。 为了也查看这些参数的名称,C ++ API 提供了named_parameters()
方法,该方法返回OrderedDict
,就像在 Python 中一样:
Net net(4, 5);
for (const auto& pair : net.named_parameters()) {std::cout << pair.key() << ": " << pair.value() << std::endl;
}
我们可以再次执行以查看输出:
root@fa350df05ecf:/home/build# make && ./dcgan 11:13:48
Scanning dependencies of target dcgan
[ 50%] Building CXX object CMakeFiles/dcgan.dir/dcgan.cpp.o
[100%] Linking CXX executable dcgan
[100%] Built target dcgan
b: -0.1863
-0.8611
-0.1228
1.3269
0.9858
[ Variable[CPUFloatType]{5} ]
linear.weight: 0.0339 0.2484 0.2035 -0.2103
-0.0715 -0.2975 -0.4350 -0.1878
-0.3616 0.1050 -0.4982 0.0335
-0.1605 0.4963 0.4099 -0.2883
0.1818 -0.3447 -0.1501 -0.0215
[ Variable[CPUFloatType]{5,4} ]
linear.bias: -0.0250
0.0408
0.3756
-0.2149
-0.3636
[ Variable[CPUFloatType]{5} ]
Note
torch::nn::Module
的文档包含在模块层次结构上运行方法的完整列表中。
在转发模式下运行网络
要使用 C ++执行网络,我们只需调用我们自己定义的forward()
方法:
int main() {Net net(4, 5);std::cout << net.forward(torch::ones({2, 4})) << std::endl;
}
打印:
root@fa350df05ecf:/home/build# ./dcgan
0.8559 1.1572 2.1069 -0.1247 0.8060
0.8559 1.1572 2.1069 -0.1247 0.8060
[ Variable[CPUFloatType]{2,5} ]
模块所有权
至此,我们知道了如何使用 C ++定义模块,注册参数,注册子模块,通过parameters()
之类的方法遍历模块层次结构并最终运行模块的forward()
方法。 尽管在 C ++ API 中还有很多方法,类和主题需要使用,但我将为您提供完整菜单的文档。 我们将在稍后实现 DCGAN 模型和端到端训练管道的过程中,涉及更多概念。 在我们这样做之前,让我简要地谈谈 C ++前端为torch::nn::Module
的子类提供的_所有权模型_。
在本次讨论中,所有权模型是指模块的存储和传递方式-确定特定模块实例的所有者或所有者_。 在 Python 中,对象始终是动态分配的(在堆上),并具有引用语义。 这是非常容易使用且易于理解的。 实际上,在 Python 中,您可以很大程度上忽略对象的位置以及如何引用它们,而将精力集中在完成事情上。_
C ++是一种较低级的语言,它在此领域提供了更多选择。 这增加了复杂性,并严重影响了 C ++前端的设计和人体工程学。 特别是,对于 C ++前端中的模块,我们可以选择使用_或_值语义_或_参考语义。 第一种情况是最简单的,并且在到目前为止的示例中已进行了展示:模块对象分配在堆栈上,并在传递给函数时可以复制,移动(使用std::move
)或通过引用或指针获取:
struct Net : torch::nn::Module { };void a(Net net) { }
void b(Net& net) { }
void c(Net* net) { }int main() {Net net;a(net);a(std::move(net));b(net);c(&net);
}
对于第二种情况-参考语义-我们可以使用std::shared_ptr
。 引用语义的优势在于,就像在 Python 中一样,它减少了思考如何将模块传递给函数以及如何声明参数的认知开销(假设您在任何地方都使用shared_ptr
)。
struct Net : torch::nn::Module {};void a(std::shared_ptr<Net> net) { }int main() {auto net = std::make_shared<Net>();a(net);
}
根据我们的经验,来自动态语言的研究人员非常喜欢引用语义而不是值语义,尽管后者比 C ++更“原生”。 同样重要的是,torch::nn::Module
的设计为了要与 Python API 的人体工程学保持紧密联系,要共享所有权。 例如,采用我们先前的Net
定义(此处为简称):
struct Net : torch::nn::Module {Net(int64_t N, int64_t M): linear(register_module("linear", torch::nn::Linear(N, M))){ }torch::nn::Linear linear;
};
为了使用linear
子模块,我们想将其直接存储在我们的类中。 但是,我们还希望模块基类了解并有权访问此子模块。 为此,它必须存储对此子模块的引用。 至此,我们已经达到了共享所有权的需要。 torch::nn::Module
类和具体的Net
类都需要引用该子模块。 因此,基类将模块存储为shared_ptr
,因此具体类也必须存储。
可是等等! 在以上代码中我没有看到任何关于shared_ptr
的提示! 这是为什么? 好吧,因为std::shared_ptr<MyModule>
实在令人难受。 为了保持研究人员的生产力,我们提出了一个精心设计的方案,以隐藏shared_ptr
的提法-通常保留给值语义的好处-同时保留参考语义。 要了解它是如何工作的,我们可以看一下核心库中torch::nn::Linear
模块的简化定义(完整定义为,在此处):
struct LinearImpl : torch::nn::Module {LinearImpl(int64_t in, int64_t out);Tensor forward(const Tensor& input);Tensor weight, bias;
};TORCH_MODULE(Linear);
简而言之:该模块不是Linear
,而是LinearImpl
。 然后,宏TORCH_MODULE
定义了实际的Linear
类。 这个“生成的”类实际上是std::shared_ptr<LinearImpl>
的包装。 它是一个包装器,而不是简单的 typedef,因此,除其他事项外,构造函数仍可按预期工作,即,您仍然可以编写torch::nn::Linear(3, 4)
而不是std::make_shared<LinearImpl>(3, 4)
。 我们将由宏创建的类称为模块_持有人_。 与(共享)指针一样,您可以使用箭头运算符(例如model->forward(...)
)访问基础对象。 最终结果是一个所有权模型,该所有权模型非常类似于 Python API。 引用语义成为默认语义,但是没有额外输入std::shared_ptr
或std::make_shared
。 对于我们的Net
,使用模块持有人 API 如下所示:
struct NetImpl : torch::nn::Module {};
TORCH_MODULE(Net);void a(Net net) { }int main() {Net net;a(net);
}
这里有一个微妙的问题值得一提。 默认构造的std::shared_ptr
为“空”,即包含空指针。 什么是默认构造的Linear
或Net
? 好吧,这是一个棘手的选择。 我们可以说它应该是一个空(null)std::shared_ptr<LinearImpl>
。 但是,请记住Linear(3, 4)
与std::make_shared<LinearImpl>(3, 4)
相同。 这意味着如果我们已确定Linear linear;
应该为空指针,则将无法构造不采用任何构造函数参数或都不使用所有缺省构造函数的模块。 因此,在当前的 API 中,默认构造的模块持有人(如Linear()
)将调用基础模块的默认构造函数(LinearImpl()
)。 如果基础模块没有默认构造函数,则会出现编译器错误。 要构造空持有人,可以将nullptr
传递给持有人的构造函数。
实际上,这意味着您可以使用如先前所示的子模块,在_初始化程序列表_中注册并构造该模块:
struct Net : torch::nn::Module {Net(int64_t N, int64_t M): linear(register_module("linear", torch::nn::Linear(N, M))){ }torch::nn::Linear linear;
};
或者,您可以先使用空指针构造持有人,然后在构造函数中为其分配值(Pythonistas 更熟悉):
struct Net : torch::nn::Module {Net(int64_t N, int64_t M) {linear = register_module("linear", torch::nn::Linear(N, M));}torch::nn::Linear linear{nullptr}; // construct an empty holder
};
结论:您应该使用哪种所有权模型–哪种语义? C ++前端的 API 最能支持模块所有者提供的所有权模型。 这种机制的唯一缺点是在模块声明下方多了一行样板。 也就是说,最简单的模型仍然是 C ++模块简介中显示的值语义模型。 对于小的,简单的脚本,您也可以摆脱它。 但是,由于技术原因,您迟早会发现它并不总是受支持。 例如,序列化 API(torch::save
和torch::load
)仅支持模块支架(或普通shared_ptr
)。 因此,推荐使用模块持有人 API 和 C ++前端定义模块,此后我们将在本教程中使用此 API。
定义 DCGAN 模块
现在,我们有必要的背景和简介来定义我们要在本文中解决的机器学习任务的模块。 回顾一下:我们的任务是从 MNIST 数据集生成数字图像。 我们想使用生成对抗网络(GAN)解决此任务。 特别是,我们将使用 DCGAN 体系结构-这是同类中最早的也是最简单的一种,但是完全可以完成此任务。
Tip
您可以在存储库中找到本教程中提供的完整源代码。
什么是 GAN aGAN?
GAN 由两个不同的神经网络模型组成:生成器_和_鉴别器。 生成器从噪声分布中接收样本,其目的是将每个噪声样本转换为类似于目标分布的图像(在我们的情况下为 MNIST 数据集)。 鉴别器又从 MNIST 数据集接收_实际_图像,或从生成器接收_假_图像。 要求发出一个概率来判断特定图像的真实程度(接近1
)或伪造(接近0
)。 来自鉴别器的关于由发生器产生的图像如何真实的反馈被用来训练发生器。 鉴别器对真实性有多好的反馈将用于优化鉴别器。 从理论上讲,生成器和鉴别器之间的微妙平衡使它们串联起来得到改善,从而导致生成器生成与目标分布无法区分的图像,从而使鉴别器(那时)的敏锐眼睛冒出了散发0.5
的真实和真实可能性。 假图片。 对我们来说,最终结果是一台接收噪声作为输入并生成数字逼真的图像作为其输出的机器。
发电机模块
我们首先定义生成器模块,该模块由一系列转置的 2D 卷积,批处理归一化和 ReLU 激活单元组成。 我们在定义自己的模块的forward()
方法中显式地(在功能上)在模块之间传递输入:
struct DCGANGeneratorImpl : nn::Module {DCGANGeneratorImpl(int kNoiseSize): conv1(nn::ConvTranspose2dOptions(kNoiseSize, 256, 4).bias(false)),batch_norm1(256),conv2(nn::ConvTranspose2dOptions(256, 128, 3).stride(2).padding(1).bias(false)),batch_norm2(128),conv3(nn::ConvTranspose2dOptions(128, 64, 4).stride(2).padding(1).bias(false)),batch_norm3(64),conv4(nn::ConvTranspose2dOptions(64, 1, 4).stride(2).padding(1).bias(false)){// register_module() is needed if we want to use the parameters() method later onregister_module("conv1", conv1);register_module("conv2", conv2);register_module("conv3", conv3);register_module("conv4", conv4);register_module("batch_norm1", batch_norm1);register_module("batch_norm2", batch_norm2);register_module("batch_norm3", batch_norm3);}torch::Tensor forward(torch::Tensor x) {x = torch::relu(batch_norm1(conv1(x)));x = torch::relu(batch_norm2(conv2(x)));x = torch::relu(batch_norm3(conv3(x)));x = torch::tanh(conv4(x));return x;}nn::ConvTranspose2d conv1, conv2, conv3, conv4;nn::BatchNorm2d batch_norm1, batch_norm2, batch_norm3;
};
TORCH_MODULE(DCGANGenerator);DCGANGenerator generator(kNoiseSize);
现在我们可以在DCGANGenerator
上调用forward()
将噪声样本映射到图像。
选择的特定模块,例如nn::ConvTranspose2d
和nn::BatchNorm2d
,遵循前面概述的结构。 kNoiseSize
常数确定输入噪声矢量的大小,并将其设置为100
。 当然,超参数是通过研究生的血统发现的。
注意
在超参数的发现中,没有研究生受到伤害。 他们定期喂给 Soylent。
Note
简要介绍如何将选项传递给 C ++前端中的Conv2d
等内置模块:每个模块都有一些必需的选项,例如BatchNorm2d
的功能数量。 如果您只需要配置所需的选项,则可以将它们直接传递给模块的构造函数,例如BatchNorm2d(128)
或Dropout(0.5)
或Conv2d(8, 4, 2)
(用于输入通道数,输出通道数和内核大小)。 但是,如果需要修改其他通常默认设置的选项,例如Conv2d
的bias
,则需要构造并传递_选项_对象。 C ++前端中的每个模块都有一个关联的选项结构,称为ModuleOptions
,其中Module
是模块的名称,例如Linear
的LinearOptions
。 这就是我们上面的Conv2d
模块的工作。
鉴别模块
鉴别器类似地是卷积,批归一化和激活的序列。 但是,卷积现在是常规的卷积,而不是转置的卷积,我们使用 alpha 值为 0.2 的泄漏 ReLU 代替了普通的 ReLU。 同样,最后的激活变为 Sigmoid,将值压缩到 0 到 1 之间。然后,我们可以将这些压缩后的值解释为鉴别器分配给真实图像的概率。
为了构建鉴别器,我们将尝试不同的方法:<cite>顺序</cite>模块。 像在 Python 中一样,PyTorch 在此提供了两种用于模型定义的 API:一种功能,其中的输入通过连续的函数传递(例如,生成器模块示例),而另一种面向对象的,其中我们构建了<cite>顺序</cite>模块 包含整个模型作为子模块。 使用<cite>顺序</cite>,鉴别符将如下所示:
nn::Sequential discriminator(// Layer 1nn::Conv2d(nn::Conv2dOptions(1, 64, 4).stride(2).padding(1).bias(false)),nn::LeakyReLU(nn::LeakyReLUOptions().negative_slope(0.2)),// Layer 2nn::Conv2d(nn::Conv2dOptions(64, 128, 4).stride(2).padding(1).bias(false)),nn::BatchNorm2d(128),nn::LeakyReLU(nn::LeakyReLUOptions().negative_slope(0.2)),// Layer 3nn::Conv2d(nn::Conv2dOptions(128, 256, 4).stride(2).padding(1).bias(false)),nn::BatchNorm2d(256),nn::LeakyReLU(nn::LeakyReLUOptions().negative_slope(0.2)),// Layer 4nn::Conv2d(nn::Conv2dOptions(256, 1, 3).stride(1).padding(0).bias(false)),nn::Sigmoid());
Tip
Sequential
模块仅执行功能组合。 第一个子模块的输出成为第二个子模块的输入,第三个子模块的输出成为第四个子模块的输入,依此类推。
加载数据中
现在我们已经定义了生成器和鉴别器模型,我们需要一些可以用来训练这些模型的数据。 与 Python 一样,C ++前端也具有强大的并行数据加载器。 该数据加载器可以从数据集中读取批次数据(您可以定义自己),并提供许多配置旋钮。
Note
尽管 Python 数据加载器使用多重处理,但 C ++数据加载器实际上是多线程的,不会启动任何新进程。
数据加载器是 C ++前端data
API 的一部分,该 API 包含在torch::data::
名称空间中。 该 API 由几个不同的组件组成:
- 数据加载器类,
- 用于定义数据集的 API,
- 用于定义_转换_的 API,可以将其应用于数据集,
- 用于定义_采样器_的 API,该采样器会生成用于对数据集建立索引的索引,
- 现有数据集,变换和采样器的库。
对于本教程,我们可以使用 C ++前端附带的MNIST
数据集。 让我们为此实例化一个torch::data::datasets::MNIST
,并应用两个转换:首先,我们对图像进行归一化,以使其在-1
至+1
的范围内(从0
到1
的原始范围)。 其次,我们应用Stack
归类,它采用一批张量并将它们沿第一维堆叠为单个张量:
auto dataset = torch::data::datasets::MNIST("./mnist").map(torch::data::transforms::Normalize<>(0.5, 0.5)).map(torch::data::transforms::Stack<>());
请注意,相对于执行训练二进制文件的位置,MNIST 数据集应位于./mnist
目录中。 您可以使用此脚本下载 MNIST 数据集。
接下来,我们创建一个数据加载器并将其传递给此数据集。 为了创建一个新的数据加载器,我们使用torch::data::make_data_loader
,它返回正确类型的std::unique_ptr
(取决于数据集的类型,采样器的类型以及其他一些实现细节):
auto data_loader = torch::data::make_data_loader(std::move(dataset));
数据加载器确实提供了很多选项。 您可以在处检查全套。 例如,为了加快数据加载速度,我们可以增加工作人员的数量。 默认数字为零,这表示将使用主线程。 如果将workers
设置为2
,将产生两个线程并发加载数据。 我们还应该将批次大小从其默认值1
增大到更合理的值,例如64
(kBatchSize
的值)。 因此,让我们创建一个DataLoaderOptions
对象并设置适当的属性:
auto data_loader = torch::data::make_data_loader(std::move(dataset),torch::data::DataLoaderOptions().batch_size(kBatchSize).workers(2));
现在,我们可以编写一个循环来加载批量数据,目前我们仅将其打印到控制台:
for (torch::data::Example<>& batch : *data_loader) {std::cout << "Batch size: " << batch.data.size(0) << " | Labels: ";for (int64_t i = 0; i < batch.data.size(0); ++i) {std::cout << batch.target[i].item<int64_t>() << " ";}std::cout << std::endl;
}
在这种情况下,数据加载器返回的类型为torch::data::Example
。 此类型是一种简单的结构,其中的data
字段用于数据,而target
字段用于标签。 因为我们之前应用了Stack
归类,所以数据加载器仅返回一个这样的示例。 如果我们未应用排序规则,则数据加载器将改为生成std::vector<torch::data::Example<>>
,批处理中每个示例包含一个元素。
如果重新生成并运行此代码,则应看到类似以下内容的内容:
root@fa350df05ecf:/home/build# make
Scanning dependencies of target dcgan
[ 50%] Building CXX object CMakeFiles/dcgan.dir/dcgan.cpp.o
[100%] Linking CXX executable dcgan
[100%] Built target dcgan
root@fa350df05ecf:/home/build# make
[100%] Built target dcgan
root@fa350df05ecf:/home/build# ./dcgan
Batch size: 64 | Labels: 5 2 6 7 2 1 6 7 0 1 6 2 3 6 9 1 8 4 0 6 5 3 3 0 4 6 6 6 4 0 8 6 0 6 9 2 4 0 2 8 6 3 3 2 9 2 0 1 4 2 3 4 8 2 9 9 3 5 8 0 0 7 9 9
Batch size: 64 | Labels: 2 2 4 7 1 2 8 8 6 9 0 2 2 9 3 6 1 3 8 0 4 4 8 8 8 9 2 6 4 7 1 5 0 9 7 5 4 3 5 4 1 2 8 0 7 1 9 6 1 6 5 3 4 4 1 2 3 2 3 5 0 1 6 2
Batch size: 64 | Labels: 4 5 4 2 1 4 8 3 8 3 6 1 5 4 3 6 2 2 5 1 3 1 5 0 8 2 1 5 3 2 4 4 5 9 7 2 8 9 2 0 6 7 4 3 8 3 5 8 8 3 0 5 8 0 8 7 8 5 5 6 1 7 8 0
Batch size: 64 | Labels: 3 3 7 1 4 1 6 1 0 3 6 4 0 2 5 4 0 4 2 8 1 9 6 5 1 6 3 2 8 9 2 3 8 7 4 5 9 6 0 8 3 0 0 6 4 8 2 5 4 1 8 3 7 8 0 0 8 9 6 7 2 1 4 7
Batch size: 64 | Labels: 3 0 5 5 9 8 3 9 8 9 5 9 5 0 4 1 2 7 7 2 0 0 5 4 8 7 7 6 1 0 7 9 3 0 6 3 2 6 2 7 6 3 3 4 0 5 8 8 9 1 9 2 1 9 4 4 9 2 4 6 2 9 4 0
Batch size: 64 | Labels: 9 6 7 5 3 5 9 0 8 6 6 7 8 2 1 9 8 8 1 1 8 2 0 7 1 4 1 6 7 5 1 7 7 4 0 3 2 9 0 6 6 3 4 4 8 1 2 8 6 9 2 0 3 1 2 8 5 6 4 8 5 8 6 2
Batch size: 64 | Labels: 9 3 0 3 6 5 1 8 6 0 1 9 9 1 6 1 7 7 4 4 4 7 8 8 6 7 8 2 6 0 4 6 8 2 5 3 9 8 4 0 9 9 3 7 0 5 8 2 4 5 6 2 8 2 5 3 7 1 9 1 8 2 2 7
Batch size: 64 | Labels: 9 1 9 2 7 2 6 0 8 6 8 7 7 4 8 6 1 1 6 8 5 7 9 1 3 2 0 5 1 7 3 1 6 1 0 8 6 0 8 1 0 5 4 9 3 8 5 8 4 8 0 1 2 6 2 4 2 7 7 3 7 4 5 3
Batch size: 64 | Labels: 8 8 3 1 8 6 4 2 9 5 8 0 2 8 6 6 7 0 9 8 3 8 7 1 6 6 2 7 7 4 5 5 2 1 7 9 5 4 9 1 0 3 1 9 3 9 8 8 5 3 7 5 3 6 8 9 4 2 0 1 2 5 4 7
Batch size: 64 | Labels: 9 2 7 0 8 4 4 2 7 5 0 0 6 2 0 5 9 5 9 8 8 9 3 5 7 5 4 7 3 0 5 7 6 5 7 1 6 2 8 7 6 3 2 6 5 6 1 2 7 7 0 0 5 9 0 0 9 1 7 8 3 2 9 4
Batch size: 64 | Labels: 7 6 5 7 7 5 2 2 4 9 9 4 8 7 4 8 9 4 5 7 1 2 6 9 8 5 1 2 3 6 7 8 1 1 3 9 8 7 9 5 0 8 5 1 8 7 2 6 5 1 2 0 9 7 4 0 9 0 4 6 0 0 8 6
...
这意味着我们能够成功地从 MNIST 数据集中加载数据。
编写训练循环
现在,让我们完成示例的算法部分,并实现生成器和鉴别器之间的精妙舞蹈。 首先,我们将创建两个优化器,一个用于生成器,一个用于区分器。 我们使用的优化程序实现了 Adam 算法:
torch::optim::Adam generator_optimizer(generator->parameters(), torch::optim::AdamOptions(2e-4).beta1(0.5));
torch::optim::Adam discriminator_optimizer(discriminator->parameters(), torch::optim::AdamOptions(5e-4).beta1(0.5));
Note
在撰写本文时,C ++前端提供了实现 Adagrad,Adam,LBBFG,RMSprop 和 SGD 的优化器。 文档具有最新列表。
接下来,我们需要更新我们的训练循环。 我们将添加一个外部循环以在每个时期耗尽数据加载器,然后编写 GAN 训练代码:
for (int64_t epoch = 1; epoch <= kNumberOfEpochs; ++epoch) {int64_t batch_index = 0;for (torch::data::Example<>& batch : *data_loader) {// Train discriminator with real images.discriminator->zero_grad();torch::Tensor real_images = batch.data;torch::Tensor real_labels = torch::empty(batch.data.size(0)).uniform_(0.8, 1.0);torch::Tensor real_output = discriminator->forward(real_images);torch::Tensor d_loss_real = torch::binary_cross_entropy(real_output, real_labels);d_loss_real.backward();// Train discriminator with fake images.torch::Tensor noise = torch::randn({batch.data.size(0), kNoiseSize, 1, 1});torch::Tensor fake_images = generator->forward(noise);torch::Tensor fake_labels = torch::zeros(batch.data.size(0));torch::Tensor fake_output = discriminator->forward(fake_images.detach());torch::Tensor d_loss_fake = torch::binary_cross_entropy(fake_output, fake_labels);d_loss_fake.backward();torch::Tensor d_loss = d_loss_real + d_loss_fake;discriminator_optimizer.step();// Train generator.generator->zero_grad();fake_labels.fill_(1);fake_output = discriminator->forward(fake_images);torch::Tensor g_loss = torch::binary_cross_entropy(fake_output, fake_labels);g_loss.backward();generator_optimizer.step();std::printf("\r[%2ld/%2ld][%3ld/%3ld] D_loss: %.4f | G_loss: %.4f",epoch,kNumberOfEpochs,++batch_index,batches_per_epoch,d_loss.item<float>(),g_loss.item<float>());}
}
上面,我们首先在真实图像上评估鉴别器,为此应为其分配较高的概率。 为此,我们使用torch::empty(batch.data.size(0)).uniform_(0.8, 1.0)
作为目标概率。
Note
我们选择均匀分布在 0.8 到 1.0 之间的随机值,而不是各处的 1.0,以使鉴别器训练更加可靠。 此技巧称为_标签平滑_。
在评估鉴别器之前,我们将其参数的梯度归零。 计算完损耗后,我们通过调用d_loss.backward()
计算新的梯度来在网络中反向传播。 我们对虚假图像重复此步骤。 我们不使用数据集中的图像,而是让生成器通过为它提供一批随机噪声来为此创建伪造图像。 然后,我们将这些伪造图像转发给鉴别器。 这次,我们希望鉴别器发出低概率,最好是全零。 一旦计算了一批真实图像和一批伪造图像的鉴别器损耗,我们就可以一步一步地进行鉴别器的优化程序,以更新其参数。
为了训练生成器,我们再次首先将其梯度归零,然后在伪图像上重新评估鉴别器。 但是,这一次,我们希望鉴别器将概率分配为非常接近的概率,这将表明生成器可以生成使鉴别器认为它们实际上是真实的图像(来自数据集)。 为此,我们用全部填充fake_labels
张量。 最后,我们逐步使用生成器的优化器来更新其参数。
现在,我们应该准备在 CPU 上训练我们的模型。 我们还没有任何代码可以捕获状态或示例输出,但是我们稍后会添加。 现在,让我们观察一下我们的模型正在_做某事_ –我们稍后将根据生成的图像来验证这是否有意义。 重建和运行应打印如下内容:
root@3c0711f20896:/home/build# make && ./dcgan
Scanning dependencies of target dcgan
[ 50%] Building CXX object CMakeFiles/dcgan.dir/dcgan.cpp.o
[100%] Linking CXX executable dcgan
[100%] Built target dcga
[ 1/10][100/938] D_loss: 0.6876 | G_loss: 4.1304
[ 1/10][200/938] D_loss: 0.3776 | G_loss: 4.3101
[ 1/10][300/938] D_loss: 0.3652 | G_loss: 4.6626
[ 1/10][400/938] D_loss: 0.8057 | G_loss: 2.2795
[ 1/10][500/938] D_loss: 0.3531 | G_loss: 4.4452
[ 1/10][600/938] D_loss: 0.3501 | G_loss: 5.0811
[ 1/10][700/938] D_loss: 0.3581 | G_loss: 4.5623
[ 1/10][800/938] D_loss: 0.6423 | G_loss: 1.7385
[ 1/10][900/938] D_loss: 0.3592 | G_loss: 4.7333
[ 2/10][100/938] D_loss: 0.4660 | G_loss: 2.5242
[ 2/10][200/938] D_loss: 0.6364 | G_loss: 2.0886
[ 2/10][300/938] D_loss: 0.3717 | G_loss: 3.8103
[ 2/10][400/938] D_loss: 1.0201 | G_loss: 1.3544
[ 2/10][500/938] D_loss: 0.4522 | G_loss: 2.6545
...
移至 GPU
尽管我们当前的脚本可以在 CPU 上正常运行,但是我们都知道卷积在 GPU 上要快得多。 让我们快速讨论如何将训练转移到 GPU 上。 为此,我们需要做两件事:将 GPU 设备规范传递给我们分配给自己的张量,并通过to()
方法将所有其他张量明确复制到 C ++前端中所有张量和模块上。 实现这两者的最简单方法是在训练脚本的顶层创建torch::Device
的实例,然后将该设备传递给诸如torch::zeros
和to()
方法之类的张量工厂函数。 我们可以从使用 CPU 设备开始:
// Place this somewhere at the top of your training script.
torch::Device device(torch::kCPU);
新的张量分配,例如
torch::Tensor fake_labels = torch::zeros(batch.data.size(0));
应该更新为以device
作为最后一个参数:
torch::Tensor fake_labels = torch::zeros(batch.data.size(0), device);
对于那些不在我们手中的张量,例如来自 MNIST 数据集的张量,我们必须插入显式的to()
调用。 这表示
torch::Tensor real_images = batch.data;
变成
torch::Tensor real_images = batch.data.to(device);
并且我们的模型参数也应该移到正确的设备上:
generator->to(device);
discriminator->to(device);
Note
如果张量已经存在于提供给to()
的设备上,则该调用为空操作。 没有多余的副本。
至此,我们已经使之前的 CPU 驻留代码更加明确。 但是,现在将设备更改为 CUDA 设备也非常容易:
torch::Device device(torch::kCUDA)
现在,所有张量都将驻留在 GPU 上,并调用快速 CUDA 内核进行所有操作,而无需我们更改任何下游代码。 如果我们想指定一个特定的设备索引,则可以将其作为第二个参数传递给Device
构造函数。 如果我们希望不同的张量驻留在不同的设备上,则可以传递单独的设备实例(例如,一个在 CUDA 设备 0 上,另一个在 CUDA 设备 1 上)。 我们甚至可以动态地进行此配置,这通常对于使我们的训练脚本更具可移植性很有用:
torch::Device device = torch::kCPU;
if (torch::cuda::is_available()) {std::cout << "CUDA is available! Training on GPU." << std::endl;device = torch::kCUDA;
}
甚至
torch::Device device(torch::cuda::is_available() ? torch::kCUDA : torch::kCPU);
检查点和恢复训练状态
我们应该对训练脚本进行的最后扩充是定期保存模型参数的状态,优化器的状态以及一些生成的图像样本。 如果我们的计算机在训练过程中崩溃,则前两个将使我们能够恢复训练状态。 对于长期的训练课程,这是绝对必要的。 幸运的是,C ++前端提供了一个 API,用于对模型和优化器状态以及单个张量进行序列化和反序列化。
为此的核心 API 是torch::save(thing,filename)
和torch::load(thing,filename)
,其中thing
可以是torch::nn::Module
子类或优化程序实例,例如我们在训练脚本中拥有的Adam
对象。 让我们更新训练循环,以一定间隔检查模型和优化器状态:
if (batch_index % kCheckpointEvery == 0) {// Checkpoint the model and optimizer state.torch::save(generator, "generator-checkpoint.pt");torch::save(generator_optimizer, "generator-optimizer-checkpoint.pt");torch::save(discriminator, "discriminator-checkpoint.pt");torch::save(discriminator_optimizer, "discriminator-optimizer-checkpoint.pt");// Sample the generator and save the images.torch::Tensor samples = generator->forward(torch::randn({8, kNoiseSize, 1, 1}, device));torch::save((samples + 1.0) / 2.0, torch::str("dcgan-sample-", checkpoint_counter, ".pt"));std::cout << "\n-> checkpoint " << ++checkpoint_counter << '\n';
}
其中kCheckpointEvery
是设置为类似于100
之类的整数,以便每批100
都进行检查,而checkpoint_counter
是每次创建检查点时都会增加的计数器。
要恢复训练状态,可以在创建所有模型和优化器之后但在训练循环之前添加如下代码:
torch::optim::Adam generator_optimizer(generator->parameters(), torch::optim::AdamOptions(2e-4).beta1(0.5));
torch::optim::Adam discriminator_optimizer(discriminator->parameters(), torch::optim::AdamOptions(2e-4).beta1(0.5));if (kRestoreFromCheckpoint) {torch::load(generator, "generator-checkpoint.pt");torch::load(generator_optimizer, "generator-optimizer-checkpoint.pt");torch::load(discriminator, "discriminator-checkpoint.pt");torch::load(discriminator_optimizer, "discriminator-optimizer-checkpoint.pt");
}int64_t checkpoint_counter = 0;
for (int64_t epoch = 1; epoch <= kNumberOfEpochs; ++epoch) {int64_t batch_index = 0;for (torch::data::Example<>& batch : *data_loader) {
检查生成的图像
我们的训练脚本现已完成。 我们准备在 CPU 或 GPU 上训练 GAN。 为了检查我们训练过程的中间输出,为此我们添加了将代码样本定期保存到"dcgan-sample-xxx.pt"
文件的代码,我们可以编写一个小的 Python 脚本来加载张量并使用 matplotlib 显示它们:
from __future__ import print_function
from __future__ import unicode_literalsimport argparseimport matplotlib.pyplot as plt
import torchparser = argparse.ArgumentParser()
parser.add_argument("-i", "--sample-file", required=True)
parser.add_argument("-o", "--out-file", default="out.png")
parser.add_argument("-d", "--dimension", type=int, default=3)
options = parser.parse_args()module = torch.jit.load(options.sample_file)
images = list(module.parameters())[0]for index in range(options.dimension * options.dimension):image = images[index].detach().cpu().reshape(28, 28).mul(255).to(torch.uint8)array = image.numpy()axis = plt.subplot(options.dimension, options.dimension, 1 + index)plt.imshow(array, cmap="gray")axis.get_xaxis().set_visible(False)axis.get_yaxis().set_visible(False)plt.savefig(options.out_file)
print("Saved ", options.out_file)
现在,让我们训练模型约 30 个纪元:
root@3c0711f20896:/home/build# make && ./dcgan 10:17:57
Scanning dependencies of target dcgan
[ 50%] Building CXX object CMakeFiles/dcgan.dir/dcgan.cpp.o
[100%] Linking CXX executable dcgan
[100%] Built target dcgan
CUDA is available! Training on GPU.
[ 1/30][200/938] D_loss: 0.4953 | G_loss: 4.0195
-> checkpoint 1
[ 1/30][400/938] D_loss: 0.3610 | G_loss: 4.8148
-> checkpoint 2
[ 1/30][600/938] D_loss: 0.4072 | G_loss: 4.36760
-> checkpoint 3
[ 1/30][800/938] D_loss: 0.4444 | G_loss: 4.0250
-> checkpoint 4
[ 2/30][200/938] D_loss: 0.3761 | G_loss: 3.8790
-> checkpoint 5
[ 2/30][400/938] D_loss: 0.3977 | G_loss: 3.3315
...
-> checkpoint 120
[30/30][938/938] D_loss: 0.3610 | G_loss: 3.8084
并在图中显示图像:
root@3c0711f20896:/home/build# python display.py -i dcgan-sample-100.pt
Saved out.png
应该看起来像这样:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ogWWTaIg-1693019293368)(img/931dea1655c975ec616a9e22c80c242f.jpg)]
数字! 万岁! 现在,事情就在您的球场上:您可以改进模型以使数字看起来更好吗?
结论
希望本教程为您提供了 PyTorch C ++前端的摘要。 像 PyTorch 这样的机器学习库必然具有非常广泛的 API。 因此,有许多概念我们没有时间或空间来讨论。 但是,我建议您尝试使用该 API,并在遇到问题时查阅我们的文档,尤其是库 API 部分。 另外,请记住,只要我们能够做到,就可以期望 C ++前端遵循 Python 前端的设计和语义,因此您可以利用这一事实来提高学习率。
相关文章:

使用 PyTorch C ++前端
使用 PyTorch C 前端 PyTorch C 前端是 PyTorch 机器学习框架的纯 C 接口。 虽然 PyTorch 的主要接口自然是 Python,但此 Python API 建立于大量的 C 代码库之上,提供基本的数据结构和功能,例如张量和自动微分。 C 前端公开了纯 C 11 API&a…...

6、NoSQL的四大分类
6、NoSQL的四大分类 kv键值对 不同公司不同的实现 新浪:Redis美团:RedisTair阿里、百度:Redismemcache 文档型数据库(bson格式和json一样) MongoDB MongoDB是一个基于分布式文件存储的数据库,一般用于存储…...

(动态规划) 剑指 Offer 60. n个骰子的点数 ——【Leetcode每日一题】
❓ 剑指 Offer 60. n个骰子的点数 难度:中等 把 n 个骰子扔在地上,所有骰子朝上一面的点数之和为 s 。输入 n,打印出s的所有可能的值出现的概率。 你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点…...

ArrayList与顺序表
文章目录 一. 顺序表是什么二. ArrayList是什么三. ArrayList的构造方法四. ArrayList的常见方法4.1 add()4.2 size()4.3 remove()4.4 get()4.5 set()4.6 contains()4.7 lastIndexOf()和 indexOf()4.8 subList()4.9 clear() 以上就是ArrayList的常见方法!…...

【【萌新的STM32-22中断概念的简单补充】】
萌新的STM32学习22-中断概念的简单补充 我们需要注意的是这句话 从上面可以看出,STM32F1 供给 IO 口使用的中断线只有 16 个,但是 STM32F1 的 IO 口却远远不止 16 个,所以 STM32 把 GPIO 管脚 GPIOx.0~GPIOx.15(xA,B,C,D,E,F,G)分别对应中断…...

Java 中数据结构HashMap的用法
Java HashMap HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。 HashMap 实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步。 HashMap 是…...

Request对象和response对象
一、概念 request对象和response对象是通过Servlet容器(如Tomcat)自动创建并传递给Servlet的。 Servlet容器负责接收客户端的请求,并将请求信息封装到request对象中,然后将request对象传 递给相应的Servlet进行处理。类似地&…...

设计模式之桥接模式
文章目录 一、介绍二、案例1. 组件抽象化2. 桥梁抽象化 一、介绍 桥接模式,属于结构型设计模式。通过提供抽象与实现之间的桥接结构,把抽象化与实现化解耦,使得二者可以独立变化。 《Head First 设计模式》: 将抽象和实现放在两…...

pom.xml配置文件失效,显示已忽略的pom.xml --- 解决方案
现象: 在 Maven 创建模块Moudle时,由于开始没有正确创建好,所以把它删掉了,然后接着又创建了与一个与之前被删除的Moudle同名的Moudle时,出现了 Ignore pom.xml,并且新创建的 Module 的 pom.xml配置文件失效…...

文本编辑器Vim常用操作和技巧
文章目录 1. Vim常用操作1.1 Vim简介1.2 Vim工作模式1.3 插入命令1.4 定位命令1.5 删除命令1.6 复制和剪切命令1.7 替换和取消命令1.8 搜索和搜索替换命令1.9 保存和退出命令 2. Vim使用技巧 1. Vim常用操作 1.1 Vim简介 Vim是一个功能强大的全屏幕文本编辑器,是L…...

【算法系列篇】位运算
文章目录 前言什么是位运算算法1.判断字符是否唯一1.1 题目要求1.2 做题思路1.3 Java代码实现 2. 丢失的数字2.1 题目要求2.2 做题思路2.3 Java代码实现 3. 两数之和3.1 题目要求3.2 做题思路3.3 Java代码实现 4. 只出现一次的数字4.1 题目要求4.2 做题思路4.3 Java代码实现 5.…...

机器学习的测试和验证(Machine Learning 研习之五)
关于 Machine Learning 研习之三、四,可到秋码记录上浏览。 测试和验证 了解模型对新案例的推广效果的唯一方法是在新案例上进行实际尝试。 一种方法是将模型投入生产并监控其性能。 这很有效,但如果你的模型非常糟糕,你的用户会抱怨——这…...

RNN循环神经网络
目录 一、卷积核与循环核 二、循环核 1.循环核引入 2.循环核:循环核按时间步展开。 3.循环计算层:向输出方向生长。 4.TF描述循环计算层 三、TF描述循环计算 四、RNN使用案例 1.数据集准备 2.Sequential中RNN 3.存储模型,acc和lose…...

安防视频监控/视频集中存储/云存储平台EasyCVR无法播放HLS协议该如何解决?
视频云存储/安防监控EasyCVR视频汇聚平台基于云边端智能协同,支持海量视频的轻量化接入与汇聚、转码与处理、全网智能分发、视频集中存储等。音视频流媒体视频平台EasyCVR拓展性强,视频能力丰富,具体可实现视频监控直播、视频轮播、视频录像、…...

Docker技术--Docker的安装
1..Docker的安装方式介绍 Docker官方提供了三种方式可以实现Docker环境的安装。分别为:Script、yum、rpm。在实际的环境中建议使用yum或者是rpm。 2..Docker的yum安装 # 1.下载docker wget https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo -O /etc/yum.re…...

客户案例|MemFire Cloud助推应急管理业务,打造百万级数据可视化大屏
「导语」 硬石科技,成立于2018年,总部位于武汉,是一家专注于应急管理行业和物联感知预警算法模型的技术核心的物联网产品和解决方案提供商。硬石科技作为一家高新技术企业,持有6项发明专利,拥有100余项各类平台认证和资…...

蒲公英路由器如何设置远程打印?
现如今,打印机已经是企业日常办公中必不可少的设备,无论何时何地,总有需要用到打印的地方,包括资料文件、统计报表等等。 但若人在外地或分公司,有文件急需通过总部的打印机进行打印时,由于不在同一物理网络…...

国产自主可控C++工业软件可视化图形架构源码
关于国产自主代替的问题是当前热点,尤其是工业软件领域。 “一个功能强大的全自主C跨平台图形可视化架构对开发自主可控工业基础软件至关重要!” 作为全球领先的C工业基础图形可视化软件提供商,UCanCode软件有自己的思考,我们认…...

【linux命令讲解大全】022.网络管理工具和命令概述
文章目录 lsattr命令语法选项参数实例 nmcli补充说明语法选项OPTIONSOBJECT 实例 systemctl补充说明任务 旧指令 新指令 实例 开启防火墙22端口 从零学 python lsattr命令 用于查看文件的第二扩展文件系统属性。 语法 lsattr(选项)(参数) 选项 -E:可显示设备属…...

应急响应流程及思路
应急响应流程及思路 一:前言 对于还没有在项目中真正接触、参与过应急响应的同学来说,“应急响应”这四个字见的最多的就是建筑工地上的横幅 —— 人人懂应急,人人会响应。这里的应急响应和我们网络安全中的应急响应有着某种本质的相似&…...

网页自适应
自适应 那就要最好提前商量好 是全局自适应 或者是 局部自适应 一般网站页面纵向滚动条都是无法避免的 都是做横向适配也就是宽度 那就不能写死宽度像素 局部自适应 一般对父元素设置百分比就行 里面的子元素就设置固定像素、 比如一些登录 全局自适应 也就是要对每个元素…...

什么是Sui Kiosk,它可以做什么,如何赋能创作者?
创作者和IP持有者需要一些工具帮助他们在区块链上实现其商业模式。Sui Kiosk作为Sui上的一种原语可以满足这种需求,为创作者提供动态选项,使他们能够在任何交易场景中设置完成交易的条件。 本文将向您介绍为什么要在SuiFrens中使用Sui Kiosk,…...

【MySQL】mysql connect
目录 一、准备工作 1、创建mysql用户 2、删除用户 3、修改用户密码 3.1、自己改自己密码 3.2、root用户修改指定用户的密码 4、数据库的权限 4.1、给用户授权 4.2、回收权限 二、连接mysql client 1、安装mysql客户端库 2、验证是否引入成功 三、 mysql接口 1、初…...

基于 vue2 发布 npm包
背景:组件化开发需要,走了一遍发布npm包的过程,采用很简单的模式实现包的发布流程,记录如下。 项目参考:基于vue的时间播放器组件,并发布到npm_timeplay.js_xmy_wh的博客-CSDN博客 1、项目初始化 首先&a…...

基于Axios完成前后端分离项目数据交互
一、安装Axios npm i axios -S 封装一个请求工具:request.js import axios from axios// 创建可一个新的axios对象 const request axios.create({baseURL: http://localhost:9090, // 后端的接口地址 ip:porttimeout: 30000 })// request 拦截器 // 可以自请求…...

时序预测 | MATLAB实现基于PSO-BiLSTM、BiLSTM时间序列预测对比
时序预测 | MATLAB实现基于PSO-BiLSTM、BiLSTM时间序列预测对比 目录 时序预测 | MATLAB实现基于PSO-BiLSTM、BiLSTM时间序列预测对比效果一览基本描述程序设计参考资料 效果一览 基本描述 MATLAB实现基于PSO-BiLSTM、BiLSTM时间序列预测对比。 1.Matlab实现PSO-BiLSTM和BiLSTM…...

C# 生成唯一ID
1.首先通过nuget安装yitter.idgenerator 下面的三行代码搞定...

python怎么提取视频中的音频
目录 操作步骤 1. 安装MoviePy库: 2. 导入MoviePy库和所需的模块: 3. 提取音频: 可能遇到的问题 1. 编解码器支持: 2. 依赖项安装: 3. 文件路径问题: 4. 内存消耗: 5. 输出文件大小&a…...

学习设计模式之建造者模式,但是宝可梦
前言 作者在准备秋招中,学习设计模式,做点小笔记,用宝可梦为场景举例,有错误欢迎指出。 建造者模式 建造者模式是一种创建型模式,主要针对于某一个类有特别繁杂的属性,并且这些属性中有部分不是必须的。…...

数学建模:变异系数法
🔆 文章首发于我的个人博客:欢迎大佬们来逛逛 变异系数法 变异系数法的设计原理是: 若某项指标的数值差异较大,能明确区分开各被评价对象,说明该指标的分辨信息丰富,因而应给该指标以较大的权重…...