LongLong's Blog

分享IT技术,分享生活感悟,热爱摄影,热爱航天。

使用PyTorch解决优化问题

PyTorch作为一款用于深度学习的计算框架,其不但提供了常用的各种深度学习模型,同时提供了多维数组(张量)及运算,线性代数和自动微分相关的特性。使得其不但可以用于深度神经网络的建模,同时还可以直接作为优化器使用,其实深度神经网络训练的过程就是对其参数优化的过程。

1. 自动微分

微分操作可以通过手动微分、数值微分、符号微分、自动微分实现,其中自动微分通过计算过程中对变量的跟踪实现对导数的计算,在深度学习框架中使用的为反向传播自动微分。在PyTorch中需要跟踪计算导数的变量需要在声明是增加requires_grad,相当于明确该变量为函数的自变量而非参数变量

例如计算函数 \( f(x, y) = x^2 + 2xy \)在点(1, 2)处的导数(梯度) \[\nabla f\ = [\frac{\partial f}{\partial x}, \frac{\partial f}{\partial y}] = [2x + 2y, 2x]\] \[\nabla f|_{(1, 2)}=[6, 4]\] 使用PyTorch计算的程序如下

import torch;
#声明两个需要跟踪导数的变量
x = torch.tensor((1.), requires_grad=True);
y = torch.tensor((2.), requires_grad=True);
#计算函数值
z = x ** 2 + 2 * x * y;
#反向传播
z.backward();
#输出导数
print(x.grad, y.grad);

最终的输出为,与手动微分计算的结果一致

tensor(6.) tensor(2.)

2. 优化问题

所谓优化问题,本质就是求函数的极值问题,对于连续可微问题则极值点的导数为零,优化问题将转换为方程求根问题。常用的方法为梯度下降法,即沿着梯度下降的方向寻找下一个位置。PyTorch中提供了各种常用的优化算法,在torch.optim包中,其中包含了部分自适应步长的算法。

例如计算函数\( f(x, y) = x^2 + 2x + y^2 \)的极小值

import torch;

x = torch.tensor((1.), requires_grad=True);
y = torch.tensor((2.), requires_grad=True);
#定义优化器,x y为进行优化的变量
optim = torch.optim.Adam([x, y], lr=1e-1);
#迭代优化
for i in range(1, 2000):
    #计算函数值
    z = x ** 2 + 2 * x + y ** 2;
    #梯度置零
    optim.zero_grad();
    #反向传播
    z.backward();
    #跟新优化变量
    optim.step();
    print(x.item(), y.item(), z.item());

最终得到的输出为

-1.0 0.0 -1.0

对于更一般的优化问题,除了函数本身还会有一些约束条件,对于带有约束条件的问题,在数学分析中可通过增加拉格朗日乘子的方法将其转换为不含约束的优化问题。

然而如果将乘子也作为其中的一个优化变量会导致构造的带有乘子的函数梯度为零的位置不是带有乘子函数的极值,考虑以下问题

求函数的极值 \[ f(x, y) = x^2 + y^2 \] 约束条件: \[ x + y - 1 = 0 \] 若采用乘子法,则 \[L = x^2 + y^2 + \lambda(x + y - 1)\] \[\frac{\partial L}{\partial x} = 2x + \lambda = \frac{\partial L}{\partial y} = 2y + \lambda = \frac{\partial L}{\partial \lambda} = x + y - 1 = 0\] 得到\(x = y = \frac{1}{2}, \lambda = -1\)

而如果使用以下程序处理该问题,会出现无法收敛的情况,说明梯度为零的点并不是函数L的极值点

import torch;

x = torch.tensor((1.), requires_grad=True);
y = torch.tensor((2.), requires_grad=True);
a = torch.tensor((0.), requires_grad=True);
optim = torch.optim.Adam([x, y, a], lr=1e-1);
for i in range(1, 2000):
    z = x ** 2 + y ** 2 + a * (x + y - 1)
    optim.zero_grad();
    z.backward();
    optim.step();
    print(x.item(), y.item(), a.item(), z.item());

为此为了程序实现上的便利可以采用罚函数法,考虑以下函数 \[ L = x^2 + y^2 + M(x + y - 1)^2\] 其中M为一个非常大的正数,此时当约束条件不满足时,函数L就会大幅增大,迫使沿着梯度下降时会自动落入满足边界条件的区域内,如此处理后程序变为

import torch;

x = torch.tensor((0.), requires_grad=True);
y = torch.tensor((0.), requires_grad=True);
optim = torch.optim.Adam([x, y], lr=1e-1);
for i in range(1, 2000):
    #增加约束条件的罚函数
    z = x ** 2 + y ** 2 + 1e5 * (x + y - 1) ** 2;
    optim.zero_grad();
    z.backward();
    optim.step();
    print(x.item(), y.item(), z.item());

最后将得到结果,非常接近使用解析的方法得到的结果

0.49999749660491943 0.49999749660491943 0.49999749660491943

Windows下的Linux子系统

适用于Linux的Windows子系统(Windows Subsystem for Linux, WSL)可让开发人员按原样运行GNU/Linux环境,包括大多数命令行工具、实用工具和应用程序。相比传统的虚拟机,WSL启动速度要快很多,相比Cygwin之类模拟器,WSL可以运行原生的Linux程序,同时WSL可以直接与Windows系统互通,可以在子系统环境中直接调用Windows程序,非常方便。

1. WSL的安装

最新版本的Windows 10已经可以完全支持WSL,微软给出的要求是Windows 10版本2004及更高版本(内部版本 19041 及更高版本)或Windows 11。安装方法为在“设置”中找到“启动或关闭Windows功能”,开启其中“适用于Linux的Windows子系统”和“虚拟机平台”两个功能,重启系统即可。

WSL有WSL1和WSL2之分,两个版本的运行方式有较大的差异:

WSL1更像是Cygwin,其和Windows环境不存在边界,即在Windows环境下可以直接看到子系统下的文件,带来的问题则是IO性能极差(应该是ext4到NFS文件系统的中间层消耗了大量的性能),解压一个40MB的文件居然需要20多分钟,并且CPU满负载运行。

WSL2则更像是虚拟机,其和Windows之间存在可见的边界,具有独立的网络和文件系统,文件系统也是一个虚拟硬盘文件,不过可以通过Windows的网络访问子系统内部的文件,相比使用sftp或是smb也还算是比较方便。今年好像还做到了在子系统内调用Windows下GPU的功能,但需要安装专门的GPU驱动。

2. 安装WSL系统

2.1 通过官方源安装

可以通过WSL命令行工具或是Microsoft store安装相应的Linux子系统,查询官方支持的Linux环境的命令如下:

wsl --list --online
以下是可安装的有效分发的列表。
请使用“wsl --install -d <分发>”安装。

NAME            FRIENDLY NAME
Ubuntu          Ubuntu
Debian          Debian GNU/Linux
kali-linux      Kali Linux Rolling
openSUSE-42     openSUSE Leap 42
SLES-12         SUSE Linux Enterprise Server v12
Ubuntu-16.04    Ubuntu 16.04 LTS
Ubuntu-18.04    Ubuntu 18.04 LTS
Ubuntu-20.04    Ubuntu 20.04 LTS

目前官方支持的Linux发行版主要以Ubuntu为主。使用wsl –install -d Ubuntu即可自动完成Linux子系统的安装。

2.2 手动安装

wsl命令行工具支持通过tar包的形式导入Linux子系统的根文件系统,以Gentoo为例,下载Gentoo的stage3包并导入的命令如下:

wget https://bouncer.gentoo.org/fetch/root/all/releases/amd64/autobuilds/20211101T222649Z/stage3-amd64-systemd-20211101T222649Z.tar.xz
wsl --import Gentoo Z:\WSL\Gentoo stage3-amd64-systemd-20211101T222649Z.tar.xz --version 2

导入命令的参数分别为导入后子系统的名称,安装路径,镜像文件的路径和使用的WSL版本,这里导入为WSL2。

3. 配置Arch Linux环境

在目前各种Linux发行版中定制化程度高,且比较易用的当属Arch Linux,下一版本的SteamOS也将基于Arch,这里介绍手动配置Arch Linux子系统的方法。

首先下载Arch Linux的最小化文件系统,当前的最新版本为archlinux-bootstrap-2021.11.01-x86_64.tar.gz,安装方式其实和Gentoo比较类似,但由于镜像文件路径的问题,先要对镜像文件进行简单的处理,由于镜像文件中存在同一个目录中具有文件名只是大小写不同的文件,为此需要在Linux环境进行处理,以root运行以下命令:

tar -xzpf archlinux-bootstrap-2021.11.01-x86_64.tar.gz
cd  root.x86_64
tar -czpf ../archlinux-bootstrap-2021.11.01-x86_64.tar.gz *

其实就是去掉镜像中root.x86_64这层目录。并升级WSL自带的内核,目前的内核是5.10,以管理员身份运行以下命令:

wsl --update

然后直接导入处理后镜像文件即可

wsl --import Arch Z:\WSL\Arch archlinux-bootstrap-2021.11.01-x86_64.tar.gz --version 2

完成后使用wsl命令可以进入安装好后的系统,并做基本的配置即可使用

wsl
#初始化pacman软件源
pacman-key --init
pacman-key --populate
pacman -Sy

在建立非root用户后,以非root用户登录是可以使用wsl命令行参数实现

#以非root用户登录,并进入home目录
wsl ~ -u longlong

4. 在Windows中访问子系统的文件

可以在文件浏览器中输入\wsl$\Arch,通过网络登录访问子系统中的文件,或者也可通过映射网络磁盘的方式进行挂载。子系统中则可以直接访问Windows的文件,全部的磁盘驱动器都会被挂在到/mnt下。

5. 在Linux子系统中使用GPU

在WSL中使用GPU需要在Windows主系统中安装对应的GPU驱动程序,御三家的WSL驱动如下,其中NVIDIA还有支持WSL的CUDA驱动

Intel GPU驱动

AMD GPU驱动

NVIDIA GPU驱动