IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 人工智能 -> pytorch官方教程中文版(二)学习PyTorch -> 正文阅读

[人工智能]pytorch官方教程中文版(二)学习PyTorch

pytorch编程环境是1.9.1+cu10.2?

建议有能力的直接看官方网站英文版

下面所示是本次教程的主要目录:

pytorch官方教程中文版:

  1. PyTorch介绍
  2. 学习PyTorch
  3. 图像和视频
  4. 声音
  5. 文本
  6. 强化学习
  7. 在生产环境中部署PyTorch模型
  8. 使用FX重构代码
  9. 前端API
  10. 扩展PyTorch
  11. 模型优化
  12. 并行和分布式训练
  13. 移动端

(二)学习PyTorch

2、1 60分钟快速入门pytorch

什么是PyTorch?

PyTorch是一个基于Python的科学计算软件包,有两个目的:

  • 替换NumPy以利用GPU和其他加速器的优势。
  • 一个自动微分库,对实现神经网络很有用。

本教程的目标:

  • 从更高层面了解PyTorch的Tensor库和神经网络。
  • 训练一个小型神经网络来对图像进行分类

确保你安装了torch和torchvision包

2、1、1 tensors

这里的tensors和第一章中的tensors内容一致。

2、1、2 torch.autograd的基本介绍

torch.autograd是PyTorch的自动微分引擎,为神经网络训练提供便利。在本节中,你将对autograd如何帮助神经网络训练有一个概念性的了解。

背景介绍

神经网络(NN)是一个嵌套函数的集合,它会在一些输入数据上执行。这些函数由参数(由权重和偏差组成)定义,在PyTorch中,这些参数被存储在tensor中。

训练NN分两步进行:

  1. 前向传播:NN对正确的输出进行最佳猜测。它通过每个函数来运行输入数据,以做出这一猜测。

  2. 后向传播:NN根据其猜测的误差来调整其参数。它通过从输出端向后遍历,收集误差相对于函数参数的导数(梯度),并使用梯度下降法优化参数。

在PyTorch中的应用

让我们来看看一个单一的训练步骤。在这个例子中,我们从Torchvision加载一个预训练的resnet18模型。我们创建一个随机数据tensor来表示一个具有3个通道、高度和宽度为64的单一图像,并将其相应的标签初始化为一些随机值。

接下来,我们通过模型的每一层来运行输入数据,进行预测。这就是前向通道。

我们使用模型的预测和相应的标签来计算误差(损失)。下一步是通过网络反向传播这个误差。当我们在误差tensor上调用.backward()时,后向传播就开始了。然后,Autograd计算并将每个模型参数的梯度存储在参数的.grad属性中。

接下来,我们加载一个优化器,在这种情况下,SGD的学习率为0.01,动量为0.9。我们在优化器中注册模型的所有参数。

最后,我们调用.step()来启动梯度下降。优化器通过存储在.grad中的梯度调整每个参数。

在这一点上,你已经拥有了训练神经网络所需的一切。下面的章节详细介绍了autograd的工作原理--你也可以选择不看。

Autograd中的微分法

让我们看看autograd是如何收集梯度的。我们创建了两个tensor a和b,规定require_grad=True。这向autograd发出信号,对它们的每一个操作都应该被跟踪。

我们从a和b创建另一个tensor Q。

让我们假设a和b是NN的参数,Q是误差。在NN训练中,我们希望误差的梯度与参数有关,即

当我们在Q上调用.backward()时,autograd会计算这些梯度并将其存储在各自tensor的.grad属性中。

我们需要在Q.backward()中明确传递一个梯度参数,因为它是一个矢量。梯度是一个与Q相同形状的tensor,它代表Q的梯度与自身的关系.

等价地,我们也可以将Q汇总成一个标量,并隐式地向后调用,比如Q.sum().backward()

梯度现在被存入a.grad和b.grad中。

计算图

从概念上讲,autograd在一个由Function对象组成的有向无环图(DAG)中保存了数据(tensor)和所有执行的操作(以及产生的新tensor)的记录。在这个DAG中,叶子是输入tensor,根部是输出tensor。通过追踪这个图从根到叶,你可以使用链式规则自动计算梯度。

在一个前向传递中,autograd同时做两件事:

  • 运行请求的操作,计算出结果的tensor,以及
  • 在DAG中维护该操作的梯度函数。

当在DAG根上调用.backward()时,后向传递开始了。

  • 计算每个.grad_fn的梯度。
  • 将它们累积到各自的tensor的 .grad 属性中,并且
  • 使用链规则,一直传播到叶子tensor。

下面是我们的例子中DAG的可视化表示。在图中,箭头的方向是向前传递的。节点代表前向传递中每个操作的后向函数。蓝色的叶子结点代表我们的叶子tensor a和b。

DAG在PyTorch中是动态的。需要注意的是,图是从头开始重新创建的;在每次调用.backward()后,autograd开始填充一个新的图。这正是允许你在模型中使用控制流语句的原因;如果需要,你可以在每次迭代时改变形状、大小和操作。

排除在DAG之外

torch.autograd 追踪所有将 require_grad 标志设置为 True 的tensor上的操作。对于不需要梯度的tensor,将此属性设置为False将其排除在梯度计算DAG之外。

即使只有一个输入tensor的 requires_grad=True,一个操作的输出tensor也需要梯度。

在NN中,不计算梯度的参数通常被称为冻结参数。如果你事先知道你不需要这些参数的梯度,那么 "冻结 "你的模型的一部分是很有用的(这通过减少自动梯度计算提供了一些性能上的好处)。

从DAG中排除的另一个常见情况是对预训练的网络进行微调。

在微调中,我们冻结了大部分模型,通常只修改分类器层以对新标签进行预测。让我们通过一个小例子来证明这一点。像以前一样,我们加载一个预训练的resnet18模型,并冻结所有的参数。

假设我们想在一个有10个标签的新数据集上微调模型。在resnet中,分类器是最后一个线性层model.fc。我们可以简单地用一个新的线性层(默认情况下是解冻的)来代替它,作为我们的分类器。

现在,除了model.fc的参数,模型中的所有参数都被冻结。唯一能计算梯度的参数是model.fc的权重和偏置。

注意尽管我们在优化器中注册了所有的参数,但唯一计算梯度的参数(因此在梯度下降中更新)是分类器的权重和偏置。

在torch.no_grad()中,同样的排除功能可以作为一个上下文管理器使用。

cvtutorials.com:这里的意思是在with torch.no_grad()代码块中,参数的梯度不会被计算。

?2、1、3 神经网络

神经网络可以用torch.nn包来构建。

现在你对autograd有了一丝了解,nn依赖于autograd来定义模型并对其进行微分。一个 nn.Module 包含层和一个返回输出的 forward(input) 方法。

例如,请看这个对数字图像进行分类的网络:

让我们试试一个随机的32x32的输入。注意:这个网络(LeNet)的预期输入尺寸是32x32。要在MNIST数据集上使用这个网络,请将数据集上的图像大小调整为32x32。

它是一个简单的前馈网络。它接受输入,一个接一个地将其送入若干层,最后给出输出。

一个典型的神经网络的训练过程如下。

  • 定义有一些可学习参数(或权重)的神经网络
  • 在输入的数据集上进行迭代
  • 通过网络处理输入
  • 计算损失(输出离正确值有多远)。
  • 将梯度传播回网络的参数中
  • 更新网络的权重,通常使用一个简单的更新规则:权重 = 权重 - 学习率 * 梯度

定义网络

让我们来定义这个网络。

你只需要定义前向函数,而后向函数(计算梯度的地方)会用autograd自动为你定义。你可以在前向函数中使用任何tensor操作。

一个模型的可学习参数由net.parameters()返回。?

让我们试试一个随机的32x32的输入。注意:这个网络(LeNet)的预期输入尺寸是32x32。要在MNIST数据集上使用这个网络,请将数据集上的图像大小调整为32x32。

将所有参数的梯度缓冲区归零,并以随机梯度进行反向传播。

注意:torch.nn只支持小批次。整个 torch.nn 包只支持小批量样本的输入,而不是单个样本。例如,nn.Conv2d将接收一个4D tensor的nSamples x nChannels x Height x Width。如果你有一个单一的样本,只需使用input.unsqueeze(0)来添加一个虚假的批次维度。

在进一步进行之前,让我们回顾一下到目前为止你所看到的所有的类。

回顾一下:

  • torch.Tensor - 一个多维数组,支持像backward()这样的autograd操作。还可以保存tensor的梯度。
  • nn.Module - 神经网络模块。封装参数的便捷方式,方便将其移动到GPU,导出,加载等。
  • nn.Parameter - 一种tensor,当分配给一个模块的属性时,会自动注册为一个参数。
  • autograd.Function - 实现autograd操作的前向和后向定义。每个Tensor操作至少创建一个Function节点,连接到创建Tensor的函数,并对其历史进行编码。

目前为止,我们涵盖了:

  • 定义一个神经网络
  • 处理输入并向后调用

还剩下:

  • 计算损失
  • 更新网络的权重

损失函数

损失函数接收(输出,目标)一对输入,并计算出一个值,估计输出与目标的距离。

nn包中有几个不同的损失函数。一个简单的损失是:nn.MSELoss,它计算的是输入和目标之间的均方误差。

比如说:?

现在,如果你按照损失的方向,使用它的.grad_fn属性,你会看到一个计算的图表,看起来像这样。

input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
      -> flatten -> linear -> relu -> linear -> relu -> linear
      -> MSELoss
      -> loss

因此,当我们调用loss.backward()时,就会得到整个图相对神经网络参数的微分,并且图中所有require_grad=True的tensor都会有其.grad tensor的梯度积累。

为了说明问题,让我们回顾一下:

反向传播

为了反向传播误差,我们所要做的就是 loss.backward()。不过你需要清除现有的梯度,否则梯度会被累积到现有的梯度上。

现在我们将调用 loss.backward(),并看看conv1在回传前后的偏置梯度。

现在,我们已经看到了如何使用损失函数。

稍后阅读:

神经网络包包含各种模块和损失函数,它们构成了深度神经网络的组成部分。这里有一个带有文档的完整列表。

现在只剩下的是:

更新网络的权重

更新权重

实践中使用的最简单的更新规则是随机梯度下降法(SGD)。

weight = weight - learning_rate * gradient

我们可以用简单的Python代码来实现这一点。

然而,由于你使用神经网络,你想使用各种不同的更新规则,如SGD、Nesterov-SGD、Adam、RMSProp等。为了实现这一点,我们建立了一个小包:torch.opt,实现了所有这些方法。使用它是非常简单的。

使用optimizer.zero_grad()观察一下梯度缓冲区是如何被手动设置为零的。这是因为梯度是累积的,正如在Backprop部分所解释的。

2、1、4 训练一个分类器

什么是数据?

一般来说,当你需要处理图像、文本、音频或视频数据时,你可以使用标准的Python包,将数据加载到一个numpy数组。然后你可以将这个数组转换为torch.*Tensor。

  • 对于图像,诸如Pillow、OpenCV等包是有用的。
  • 对于音频,诸如scipy和librosa等软件包。
  • 对于文本,原始Python或基于Cython的加载,或者NLTK和SpaCy都很有用。

具体到视觉方面,我们创建了一个叫做torchvision的包,它有常见数据集的数据加载器,如ImageNet、CIFAR10、MNIST等,以及图像的数据转换器,即torchvision.datasets和torch.utils.data.DataLoader。

这提供了巨大的便利,避免了编写模板代码。

在本教程中,我们将使用CIFAR10数据集。它有以下类别:"飞机"、"汽车"、"鸟"、"猫"、"鹿"、"狗"、"青蛙"、"马"、"船"、"卡车"。CIFAR-10中的图像大小为3x32x32,即尺寸为32x32像素的3通道彩色图像。

训练一个图像分类器

我们将按顺序进行以下步骤。

  1. 使用Torchvision加载和规范化CIFAR10训练和测试数据集
  2. 定义一个卷积神经网络
  3. 定义一个损失函数
  4. 在训练数据上训练该网络
  5. 在测试数据上测试该网络

1. 加载并归一化CIFAR10

使用torchvision,加载CIFAR10是非常容易的。

torchvision数据集的输出是范围[0, 1]的PILImage图像。我们将它们转换为归一化范围[-1, 1]的tensor。

注意:如果在Windows上运行并得到BrokenPipeError,请尝试将torch.utils.data.DataLoader()的num_worker设为0。

让我们展示一些训练图像:

2. 定义一个卷积神经网络

复制之前神经网络部分的神经网络,并修改它以获取3通道的图像(而不是之前定义的1通道的图像)。

3. 定义一个损失函数和优化器

让我们使用一个分类交叉熵损失和带momentum的SGD。

4. 训练网络

这就是事情开始变得有趣的时候。我们只需在我们的数据迭代器上进行循环,并将图像输入到网络中并进行优化。

让我们快速保存我们的训练模型。

5.在测试数据上测试网络

我们已经在训练数据集上对网络进行了两次训练。但是我们需要检查网络是否学到了任何东西。

我们将通过预测神经网络输出的类别标签来检查,并将其与正确的类别标签进行对照。如果预测是正确的,我们就把这个样本添加到正确的预测列表中。

好的,第一步。让我们从测试集上显示一张图片来熟悉一下。

接下来,让我们装回我们保存的模型(注意:保存和重新加载模型在这里不是必须的,我们这样做只是为了说明如何做)。

好了,现在让我们看看神经网络认为上面这些例子是什么。

输出是10个类别的置信度一个类别的置信度越高,网络就越认为该图像属于该特定类别。因此,让我们得到最高置信度的索引。

结果似乎很好。

让我们看看该网络在整个数据集上的表现如何。

这看起来比随机猜测的结果要好得多,随即猜测是10%的准确率(从10个类中随机挑选一个类)。似乎网络学到了一些东西。

让我们来看看哪些类预测表现好,哪些类别预测表现不好:

好吧,那么接下来呢?

我们如何在GPU上运行这些神经网络?

在GPU上进行训练

就像你如何将tensor转移到GPU上一样,你可以将神经网络转移到GPU上。

如果我们有CUDA可用,让我们首先把我们的设备定义为第一个可见的cuda设备。

本节的其余部分假设设备是一个CUDA设备。

然后,这些方法将递归所有模块,并将其参数和缓冲区转换为CUDA tensor。

请记住,你也必须将每一步的输入和目标发送到GPU。

为什么我没有注意到与CPU相比有巨大的速度提升?因为你的网络真的很小。

练习:试着增加网络的宽度(第一个nn.Conv2d的参数2和第二个nn.Conv2d的参数1--它们必须是相同的数字),看看你能加速多少。

实现的目标:

  • 从更高层面了解PyTorch的Tensor库和神经网络。
  • 训练一个小型神经网络来对图像进行分类

2、2 PyTorch案例

PyTorch案例是旧版本的教程,建议使用基础学习

2、3 torch.nn是啥?

我们建议以notebook的形式运行本教程,而不是脚本。

PyTorch提供了设计优雅的模块和类torch.nn、torch.optim、Dataset和DataLoader来帮助你创建和训练神经网络。为了充分利用它们的力量,并为你的问题定制它们,你需要真正理解它们到底在做什么。为了加深这种理解,我们将首先在MNIST数据集上训练基本的神经网络,而不使用这些模型的任何特征;我们最初将只使用最基本的PyTorch tensor功能。然后,我们将逐步从torch.nn、torch.optim、Dataset或DataLoader中添加一个特征,准确地展示每一部分代码的作用,以及它如何使代码更简洁或更灵活。

本教程假定你已经安装了PyTorch,并且熟悉tensor操作的基础知识。(如果你熟悉Numpy数组操作,你会发现这里使用的PyTorch tensor操作几乎是一样的)。)

2、3、1 MNIST数据设置

我们将使用经典的MNIST数据集,它由手绘数字(0到9之间)的黑白图像组成。

我们将使用pathlib来处理路径(Python 3标准库的一部分),并使用request下载数据集。我们将只在使用模块时才导入模块,所以你可以清楚地看到在每个部分代码使用的是什么模块。

from pathlib import Path
import requests

DATA_PATH = Path("data")
PATH = DATA_PATH / "mnist"

PATH.mkdir(parents=True, exist_ok=True)

URL = "https://github.com/pytorch/tutorials/raw/master/_static/"
FILENAME = "mnist.pkl.gz"

if not (PATH / FILENAME).exists():
        content = requests.get(URL + FILENAME).content
        (PATH / FILENAME).open("wb").write(content)

这个数据集是numpy数组格式,并使用pickle存储,这是一种python特有的数据序列化格式。

import pickle
import gzip

with gzip.open((PATH / FILENAME).as_posix(), "rb") as f:
        ((x_train, y_train), (x_valid, y_valid), _) = pickle.load(f, encoding="latin-1")

每张图片是28 x 28,被存储为长度为784(=28x28)的一行。让我们看看其中一张;我们需要先把它重整为符合条件的2d图像。

from matplotlib import pyplot
import numpy as np

pyplot.imshow(x_train[0].reshape((28, 28)), cmap="gray")
print(x_train.shape)

(50000, 784)

PyTorch使用torch.tensor,而不是numpy数组,所以我们需要转换数据。

import torch

x_train, y_train, x_valid, y_valid = map(
    torch.tensor, (x_train, y_train, x_valid, y_valid)
)
n, c = x_train.shape
print(x_train, y_train)
print(x_train.shape)
print(y_train.min(), y_train.max())
tensor([[0., 0., 0.,  ..., 0., 0., 0.],
        [0., 0., 0.,  ..., 0., 0., 0.],
        [0., 0., 0.,  ..., 0., 0., 0.],
        ...,
        [0., 0., 0.,  ..., 0., 0., 0.],
        [0., 0., 0.,  ..., 0., 0., 0.],
        [0., 0., 0.,  ..., 0., 0., 0.]]) tensor([5, 0, 4,  ..., 8, 4, 8])
torch.Size([50000, 784])
tensor(0) tensor(9)

2、3、2 从0开始创建神经网络(无Torch.nn)

让我们首先只使用PyTorch的tensor操作创建一个模型。我们假设你已经熟悉了神经网络的基础知识。

PyTorch提供了创建随机或零填充tensor的方法,我们将使用这些方法为一个简单的线性模型创建我们的权重和偏置,这些只是普通的tensor,还有一点:我们需要告诉PyTorch它们需要一个梯度。这样PyTorch会对tensor所有操作进行记录,这样它就可以在反向传播过程中自动计算梯度了

对于权重,我们在初始化后设置requirest_grad,因为我们不希望梯度中包含这一步。(请注意,PyTorch中的尾部_标志着该操作是原位进行的)

我们在这里用Xavier初始化(通过乘以1/sqrt(n))来初始化权重。

import math

weights = torch.randn(784, 10) / math.sqrt(784)
weights.requires_grad_()
bias = torch.zeros(10, requires_grad=True)

由于PyTorch具有自动计算梯度的能力,我们可以使用任何标准的Python函数(或可调用对象)作为模型。所以我们只需写一个普通的矩阵乘法和广播式加法就可以创建一个简单的线性模型。我们还需要一个激活函数,所以我们将编写log_softmax并使用它。记住:虽然PyTorch提供了很多内置的损失函数、激活函数等,但你也可以用普通的python轻松地写出你自己的函数。PyTorch甚至会自动为你的函数创建快速的GPU或矢量的CPU代码。

def log_softmax(x):
    return x - x.exp().sum(-1).log().unsqueeze(-1)

def model(xb):
    return log_softmax(xb @ weights + bias)

在上面,@代表点积操作。我们将在一批数据上调用我们的函数(在本例中,64张图片)。这就是一个前向传递。请注意,我们的预测在这个阶段不会比随机好,因为我们从随机权重开始。

bs = 64  # batch size

xb = x_train[0:bs]  # a mini-batch from x
preds = model(xb)  # predictions
preds[0], preds.shape
print(preds[0], preds.shape)
tensor([-2.4499, -2.2839, -2.7530, -2.0148, -2.7415, -2.0347, -2.4802, -2.1444,
        -2.1444, -2.2874], grad_fn=<SelectBackward>) torch.Size([64, 10])

正如你所看到的,preds tensor不仅包含tensor值,而且还包含一个梯度函数。我们稍后会用它来做反向传播。

让我们实现负对数似然来作为损失函数(同样,我们可以直接使用标准Python)。

def nll(input, target):
    return -input[range(target.shape[0]), target].mean()

loss_func = nll
让我们用我们的随机模型计算一下我们的损失,这样我们就可以看到我们在稍后的反向传播过程中是否有所改进。
yb = y_train[0:bs]
print(loss_func(preds, yb))
tensor(2.2745, grad_fn=<NegBackward>)

让我们也实现一个函数来计算我们模型的准确性。对于每个预测,如果数值最大的索引与目标值相匹配,那么预测是正确的。

def accuracy(out, yb):
    preds = torch.argmax(out, dim=1)
    return (preds == yb).float().mean()

让我们检查一下我们的随机模型的准确性,这样我们就可以看到我们的准确性是否随着我们的损失减小而提高。

print(accuracy(preds, yb))
tensor(0.1094)

我们现在可以运行一个训练循环。对于每个迭代,我们将:

  • 选择一个小批量的数据(大小为bs)。
  • 使用该模型进行预测
  • 计算损失
  • loss.backward()更新模型的梯度,在这里是指权重和偏置。

我们现在使用这些梯度来更新权重和偏置。我们在torch.no_grad()上下文管理器中做这件事,因为我们不希望这些行为被记录在我们下一次计算梯度的时候。

然后我们将梯度设置为零,这样我们就可以为下一个循环做好准备。否则,我们的梯度将记录所有已经发生的操作的流水帐(即 loss.backward() 将梯度添加到已经存储的东西中,而不是替换它们)。

小贴士:您可以使用标准的Python调试器来浏览PyTorch的代码,允许您在每个步骤中检查各种变量值。取消下面的set_trace()来尝试一下。

from IPython.core.debugger import set_trace

lr = 0.5  # learning rate
epochs = 2  # how many epochs to train for

for epoch in range(epochs):
    for i in range((n - 1) // bs + 1):
        #         set_trace()
        start_i = i * bs
        end_i = start_i + bs
        xb = x_train[start_i:end_i]
        yb = y_train[start_i:end_i]
        pred = model(xb)
        loss = loss_func(pred, yb)

        loss.backward()
        with torch.no_grad():
            weights -= weights.grad * lr
            bias -= bias.grad * lr
            weights.grad.zero_()
            bias.grad.zero_()

就这样:我们完全从头开始创建并训练了一个最小的神经网络(在这种情况下,是一个逻辑回归,因为我们没有隐藏层)!

让我们检查一下损失和准确率,并将其与我们之前得到的数据进行比较。我们预计损失会减少,准确率会提高,它们确实如此。

print(loss_func(model(xb), yb), accuracy(model(xb), yb))
tensor(0.0806, grad_fn=<NegBackward>) tensor(1.)

2、3、3 使用Torch.nn.functional

现在,我们将重构我们的代码,使其与之前做的事情相同,只是我们将开始利用PyTorch的nn类,使其更加简洁和灵活。从这里开始的每一步,我们都应该使我们的代码有一个或多个特点:更短、更容易理解和/或更灵活。

第一步,也是最简单的一步,就是用torch.nn.functional(按照惯例,它通常被导入到命名空间F中)的函数取代我们手写的激活和损失函数,使我们的代码更短。这个模块包含了torch.nn库中的所有函数(而库的其他部分包含了类)。除了大量的损失和激活函数外,你还会在这里找到一些创建神经网络的会用到的函数,如池化函数。(也有一些函数用于做卷积、线性层等,但正如我们将看到的,这些通常用库的其他部分处理会更好)。

如果你使用的是负对数似然损失和对数softmax激活,那么Pytorch提供了一个单一的函数F.cross_entropy来结合这两者。所以我们甚至可以从我们的模型中删除激活函数。

import torch.nn.functional as F

loss_func = F.cross_entropy

def model(xb):
    return xb @ weights + bias

注意,我们不再在模型函数中调用log_softmax。让我们确认一下,我们的损失和准确率与以前一样。

print(loss_func(model(xb), yb), accuracy(model(xb), yb))
tensor(0.0806, grad_fn=<NllLossBackward>) tensor(1.)

2、3、4 使用nn.Module进行重构

接下来,我们将使用 nn.Module 和 nn.Parameter,以获得更清晰、更简洁的训练循环。我们对nn.Module进行子类化(它本身就是一个类,能够保持对状态的跟踪)。在这种情况下,我们要创建一个类来保存我们的权重、偏差和forward步骤的方法。nn.Module有许多属性和方法(如.parameters()和.zero_grad()),我们将使用它们。

注意:nn.Module(大写M)是PyTorch特有的概念,也是我们将经常使用的一个类。nn.Module不能与Python中的模块(小写m)概念相混淆,后者是一个可以导入的Python代码文件。

from torch import nn

class Mnist_Logistic(nn.Module):
    def __init__(self):
        super().__init__()
        self.weights = nn.Parameter(torch.randn(784, 10) / math.sqrt(784))
        self.bias = nn.Parameter(torch.zeros(10))

    def forward(self, xb):
        return xb @ self.weights + self.bias

由于我们现在使用的是一个对象,而不仅仅是使用一个函数,所以我们首先要把我们的模型实例化。

model = Mnist_Logistic()

现在我们可以用和以前一样的方式计算损失。注意,nn.Module对象被当作函数来使用(即它们是可调用的),但在幕后Pytorch会自动调用我们的forward方法。

print(loss_func(model(xb), yb))
tensor(2.4699, grad_fn=<NllLossBackward>)

以前,对于我们的训练循环,我们必须按名称更新每个参数的值,并手动为每个参数分别调零,像这样。

with torch.no_grad():
    weights -= weights.grad * lr
    bias -= bias.grad * lr
    weights.grad.zero_()
    bias.grad.zero_()

现在我们可以利用model.parameters()和model.zero_grad()(它们都是由PyTorch为nn.Module定义的),使这些步骤更加简洁,不容易出现忘记一些参数的错误,特别是当我们有一个比较复杂的模型时。

with torch.no_grad():
    for p in model.parameters(): p -= p.grad * lr
    model.zero_grad()

我们将把我们的小训练循环包裹在一个fit函数中,这样我们以后就可以再次运行它。

def fit():
    for epoch in range(epochs):
        for i in range((n - 1) // bs + 1):
            start_i = i * bs
            end_i = start_i + bs
            xb = x_train[start_i:end_i]
            yb = y_train[start_i:end_i]
            pred = model(xb)
            loss = loss_func(pred, yb)

            loss.backward()
            with torch.no_grad():
                for p in model.parameters():
                    p -= p.grad * lr
                model.zero_grad()

fit()

让我们仔细检查一下,我们的损失已经减少了。

print(loss_func(model(xb), yb))
tensor(0.0833, grad_fn=<NllLossBackward>)

2、3、5 使用nn.Linear进行重构

我们继续重构我们的代码。我们不再手动定义和初始化self.weights和self.bias,并计算xb @ self.weights + self.bias,而是使用Pytorch的nn.Linear类的线性层,它为我们做了所有这些。Pytorch有很多类型的预定义层,可以大大简化我们的代码,而且通常也会使代码更快。

class Mnist_Logistic(nn.Module):
    def __init__(self):
        super().__init__()
        self.lin = nn.Linear(784, 10)

    def forward(self, xb):
        return self.lin(xb)

我们实例化我们的模型,并以与之前相同的方式计算损失。

model = Mnist_Logistic()
print(loss_func(model(xb), yb))
tensor(2.4067, grad_fn=<NllLossBackward>)

我们仍然能够像以前一样使用我们的fit方法。

fit()

print(loss_func(model(xb), yb))
tensor(0.0820, grad_fn=<NllLossBackward>)

2、3、6 使用optim进行重构

Pytorch也有一个包含各种优化算法的包,torch.opt。我们可以使用优化器中的step方法来改进前面的代码,而不是手动更新每个参数。

这将让我们取代之前手动编写代码的优化步骤。

with torch.no_grad():
    for p in model.parameters(): p -= p.grad * lr
    model.zero_grad()

仅仅使用:

opt.step()
opt.zero_grad()

(optim.zero_grad()将梯度重置为0,我们需要在计算下一个minibatch的梯度之前调用它。)

from torch import optim

我们将定义一个小函数来创建我们的模型和优化器,这样我们就可以在后面重复使用它。

def get_model():
    model = Mnist_Logistic()
    return model, optim.SGD(model.parameters(), lr=lr)

model, opt = get_model()
print(loss_func(model(xb), yb))

for epoch in range(epochs):
    for i in range((n - 1) // bs + 1):
        start_i = i * bs
        end_i = start_i + bs
        xb = x_train[start_i:end_i]
        yb = y_train[start_i:end_i]
        pred = model(xb)
        loss = loss_func(pred, yb)

        loss.backward()
        opt.step()
        opt.zero_grad()

print(loss_func(model(xb), yb))
tensor(2.3580, grad_fn=<NllLossBackward>)
tensor(0.0816, grad_fn=<NllLossBackward>)

2、3、7 使用Dataset进行重构

PyTorch有一个抽象的数据集类。一个数据集可以是任何具有__len__函数(由Python的标准len函数调用)和__getitem__函数作为索引的方式的东西。本教程讲述了一个创建自定义FacialLandmarkDataset类作为Dataset的一个子类的好例子。

PyTorch的TensorDataset是一个包裹tensor的数据集。通过定义长度和索引方式,这也给了我们一种沿tensor的第一维进行迭代、索引和切片的方法。这将使我们在训练时更容易在同一行中访问自变量和因变量。

from torch.utils.data import TensorDataset

x_train和y_train都可以合并在一个TensorDataset中,这将更容易迭代和分片。

train_ds = TensorDataset(x_train, y_train)

以前,我们不得不通过小批量分别迭代X和Y值。

xb = x_train[start_i:end_i]
yb = y_train[start_i:end_i]

现在,我们可以把这两步一起做。

xb,yb = train_ds[i*bs : i*bs+bs]
model, opt = get_model()

for epoch in range(epochs):
    for i in range((n - 1) // bs + 1):
        xb, yb = train_ds[i * bs: i * bs + bs]
        pred = model(xb)
        loss = loss_func(pred, yb)

        loss.backward()
        opt.step()
        opt.zero_grad()

print(loss_func(model(xb), yb))
tensor(0.0817, grad_fn=<NllLossBackward>)

2、3、8 使用DataLoader进行重构

Pytorch的DataLoader负责管理批量样本。你可以从任何Dataset创建一个DataLoader。DataLoader使得迭代批量样本变得更加容易。不使用train_ds[ibs : ibs+bs],DataLoader会自动给我们每个minibatch。

from torch.utils.data import DataLoader

train_ds = TensorDataset(x_train, y_train)
train_dl = DataLoader(train_ds, batch_size=bs)

以前,我们的循环是这样迭代批次(xb, yb)的。

for i in range((n-1)//bs + 1):
    xb,yb = train_ds[i*bs : i*bs+bs]
    pred = model(xb)

现在,我们的循环干净多了,因为(xb, yb)是自动从数据加载器加载的。

for xb,yb in train_dl:
    pred = model(xb)
model, opt = get_model()

for epoch in range(epochs):
    for xb, yb in train_dl:
        pred = model(xb)
        loss = loss_func(pred, yb)

        loss.backward()
        opt.step()
        opt.zero_grad()

print(loss_func(model(xb), yb))
tensor(0.0825, grad_fn=<NllLossBackward>)

多亏Pytorch的nn.Module、nn.Parameter、Dataset和DataLoader,我们的训练循环现在已经大大缩小,而且更容易理解。现在,让我们尝试添加在实践中创建有效模型所需的基本功能。

2、3、9 添加验证

在第1节中,我们只是试图得到一个合理的训练循环,用于我们的训练数据。在现实中,你总是应该有一个验证集,以确定你是否过度拟合。

将训练数据洗牌对于防止批次之间的关联性和过度拟合是很重要的。另一方面,无论我们是否对验证集进行洗牌,验证损失都是相同的。由于洗牌需要额外的时间,所以洗牌验证数据是没有意义的。

我们对验证集使用的批样本数量(batch size)是训练集的两倍。这是因为验证集不需要反向传播,因此占用的内存较少(它不需要存储梯度)。我们利用这一点,使用更大的批样本量,更快速地计算损失。

train_ds = TensorDataset(x_train, y_train)
train_dl = DataLoader(train_ds, batch_size=bs, shuffle=True)

valid_ds = TensorDataset(x_valid, y_valid)
valid_dl = DataLoader(valid_ds, batch_size=bs * 2)

我们将在每个epoch结束时计算并打印验证损失。

(注意,我们总是在训练前调用model.train(),在推理前调用model.eval(),因为这些被nn.BatchNorm2d和nn.Dropout等层使用,以确保这些不同阶段的适当行为)。)

model, opt = get_model()

for epoch in range(epochs):
    model.train()
    for xb, yb in train_dl:
        pred = model(xb)
        loss = loss_func(pred, yb)

        loss.backward()
        opt.step()
        opt.zero_grad()

    model.eval()
    with torch.no_grad():
        valid_loss = sum(loss_func(model(xb), yb) for xb, yb in valid_dl)

    print(epoch, valid_loss / len(valid_dl))
0 tensor(0.6554)
1 tensor(0.2832)

2、3、10 创建fit()和get_data()

我们现在要做一点我们自己代码的重构。由于我们经历了两次类似的计算训练集和验证集的损失的过程,让我们把它变成自己的函数,即 loss_batch,它计算一个批次的损失。

我们为训练集传入一个优化器,并使用它来执行反向传播。对于验证集,我们没有传入优化器,所以该方法不执行反向传播。

def loss_batch(model, loss_func, xb, yb, opt=None):
    loss = loss_func(model(xb), yb)

    if opt is not None:
        loss.backward()
        opt.step()
        opt.zero_grad()

    return loss.item(), len(xb)

fit运行必要的操作来训练我们的模型,并计算每个epoch的训练和验证损失。

import numpy as np

def fit(epochs, model, loss_func, opt, train_dl, valid_dl):
    for epoch in range(epochs):
        model.train()
        for xb, yb in train_dl:
            loss_batch(model, loss_func, xb, yb, opt)

        model.eval()
        with torch.no_grad():
            losses, nums = zip(
                *[loss_batch(model, loss_func, xb, yb) for xb, yb in valid_dl]
            )
        val_loss = np.sum(np.multiply(losses, nums)) / np.sum(nums)

        print(epoch, val_loss)

get_data返回训练集和验证集的dataloaders。

def get_data(train_ds, valid_ds, bs):
    return (
        DataLoader(train_ds, batch_size=bs, shuffle=True),
        DataLoader(valid_ds, batch_size=bs * 2),
    )

现在,我们获取数据加载器和拟合模型的整个过程可以在3行代码中实现:

train_dl, valid_dl = get_data(train_ds, valid_ds, bs)
model, opt = get_model()
fit(epochs, model, loss_func, opt, train_dl, valid_dl)
0 0.3153906076908112
1 0.29883544899225234

你可以使用这些基本的3行代码来训练各种各样的模型。让我们看看是否可以用它们来训练一个卷积神经网络(CNN)!

2、3、11 切换到CNN

我们现在要用三个卷积层建立我们的神经网络。因为上一节中的所有函数都没有对模型的形式做任何假设,我们将能够使用它们来训练一个CNN,而不需要做任何修改。

我们将使用Pytorch的预定义Conv2d类作为我们的卷积层。我们定义一个有3个卷积层的CNN。每个卷积层之后都有一个ReLU。最后,我们进行一次平均池化。(注意,view是PyTorch的函数,类似numpy的reshape)

class Mnist_CNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1)
        self.conv2 = nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1)
        self.conv3 = nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1)

    def forward(self, xb):
        xb = xb.view(-1, 1, 28, 28)
        xb = F.relu(self.conv1(xb))
        xb = F.relu(self.conv2(xb))
        xb = F.relu(self.conv3(xb))
        xb = F.avg_pool2d(xb, 4)
        return xb.view(-1, xb.size(1))

lr = 0.1

动量是随机梯度下降法的一个变种,它也考虑到了以前的更新,通常会导致更快的训练。

model = Mnist_CNN()
opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)

fit(epochs, model, loss_func, opt, train_dl, valid_dl)
0 0.41149860814809797
1 0.24853566836118698

2、3、12?nn.Sequential

torch.nn有另一个方便的类,我们可以用它来简化我们的代码:Sequential 。一个Sequential对象以顺序的方式运行其中的每个模块。这是一种更简单的编写神经网络的方式。

为了利用这一点,我们需要能够轻松地从一个给定的函数中定义一个自定义层。例如,PyTorch没有一个视图层,我们需要为我们的网络创建一个。Lambda将创建一个层,然后我们可以在用Sequential定义网络时使用。

class Lambda(nn.Module):
    def __init__(self, func):
        super().__init__()
        self.func = func

    def forward(self, x):
        return self.func(x)


def preprocess(x):
    return x.view(-1, 1, 28, 28)

用Sequential创建的模型是简单的。

model = nn.Sequential(
    Lambda(preprocess),
    nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    nn.AvgPool2d(4),
    Lambda(lambda x: x.view(x.size(0), -1)),
)

opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)

fit(epochs, model, loss_func, opt, train_dl, valid_dl)
0 0.36114372408390044
1 0.2488429978251457

2、3、13 包裹DataLoader

我们的CNN是相当简洁的,但它只适用于MNIST,因为。

  • 它假定输入是一个28*28的长矢量
  • 它假设最终的CNN网格大小为4*4(因为这是我们使用的平均集合核大小)

让我们摆脱这两个假设,使我们的模型适用于任何2d单通道图像。首先,我们可以通过将数据预处理移到生成器中来删除初始Lambda层。

def preprocess(x, y):
    return x.view(-1, 1, 28, 28), y


class WrappedDataLoader:
    def __init__(self, dl, func):
        self.dl = dl
        self.func = func

    def __len__(self):
        return len(self.dl)

    def __iter__(self):
        batches = iter(self.dl)
        for b in batches:
            yield (self.func(*b))

train_dl, valid_dl = get_data(train_ds, valid_ds, bs)
train_dl = WrappedDataLoader(train_dl, preprocess)
valid_dl = WrappedDataLoader(valid_dl, preprocess)

接下来,我们可以用 nn.AdaptiveAvgPool2d 替换 nn.Avool2d,它允许我们定义我们想要的输出tensor的大小,而不是我们的输入tensor。因此,我们的模型将适用于任何大小的输入。

model = nn.Sequential(
    nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    nn.AdaptiveAvgPool2d(1),
    Lambda(lambda x: x.view(x.size(0), -1)),
)

opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)

让我们来试一试。

fit(epochs, model, loss_func, opt, train_dl, valid_dl)
0 0.3379354884147644
1 0.21907222011089325

2、3、14 使用你的GPU

如果你足够幸运,可以使用具有CUDA功能的GPU(你可以从大多数云提供商那里以大约0.50美元/小时的价格租用一个),你可以使用它来加速你的代码。首先检查你的GPU是否在Pytorch中工作。

print(torch.cuda.is_available())
True

然后为其创建一个设备对象。

dev = torch.device(
    "cuda") if torch.cuda.is_available() else torch.device("cpu")

让我们更新preprocess,将批样本转移到GPU上。

def preprocess(x, y):
    return x.view(-1, 1, 28, 28).to(dev), y.to(dev)


train_dl, valid_dl = get_data(train_ds, valid_ds, bs)
train_dl = WrappedDataLoader(train_dl, preprocess)
valid_dl = WrappedDataLoader(valid_dl, preprocess)

最后,我们可以把我们的模型移到GPU上。

model.to(dev)
opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)

你应该发现它现在运行得更快。

fit(epochs, model, loss_func, opt, train_dl, valid_dl)
0 0.20114168793559076
1 0.15547060708403587

2、3、15 结束语

我们现在有一个通用的数据管道和训练循环,你可以用Pytorch来训练许多类型的模型。要想知道现在训练一个模型有多简单,可以看看mnist_sample案例的notebook。

当然,你会想添加很多东西,比如数据增强、超参数调整、监控训练、转移学习等等。这些功能在fastai库中都有,该库是用本教程中展示的同样的设计方法开发的,为希望进一步改进模型的从业者提供了自然的下一步。

我们在本教程开始时承诺,我们将通过实例解释torch.nn、torch.optim、Dataset和DataLoader。所以让我们总结一下我们所看到的:

  • torch.nn: Module:创建一个类似于函数的可调用文件,但也可以包含状态(如神经网层权重)。它知道它所包含的Parameter,并可以将它们的梯度归零,通过它们进行权重更新的循环,等等; Parameter:一个tensor的包装器,告诉模块它有需要在后向传播过程中更新的权重。只有设置了require_grad属性的tensor才会被更新; functional: 一个模块(通常按惯例导入到F命名空间),包含激活函数、损失函数等,以及卷积层和线性层等非状态化版本。
  • torch.optim:包含优化器,如SGD,它在后向传播步骤中更新Parameters的权重。
  • Dataset:一个具有__len__和__getitem__对象的抽象接口,包括Pytorch提供的类,如TensorDataset
  • DataLoader:接受任何Dataset并创建一个迭代器,返回批样本。

2、4 可视化模型、数据,用TensorBoard训练数据

在《60分钟入门》中,我们向你展示了如何加载数据,将数据送入到我们定义为nn.Module子类的模型,在训练数据上训练这个模型,并在测试数据上测试它。为了了解发生了什么,我们在模型训练时打印出一些统计数据,以了解训练是否有进展。然而,我们可以做得比这更好。PyTorch与TensorBoard集成,TensorBoard是一个专门用于可视化神经网络训练结果的工具。本教程使用Fashion-MNIST数据集说明了它的一些功能,该数据集可以通过torchvision.datasets读入。

安装tensorboard,在anaconda环境下,输入pip install tb-nightly

在本教程中,我们将学习如何:

  1. 读取数据并进行适当的转换(几乎与之前的教程相同)。
  2. 设置TensorBoard。
  3. 写入TensorBoard。
  4. 使用TensorBoard检查一个模型架构。
  5. 使用TensorBoard来创建我们在上一个教程中创建的可视化的交互式版本,代码更少。

具体来说,在第5点,我们会看到:

  • 检查我们训练数据的几种方法
  • 如何在训练过程中跟踪我们模型的性能
  • 一旦我们的模型训练完成,如何评估它的性能

我们将从类似于CIFAR-10教程中的模板代码开始:

我们将从该教程中定义一个类似的模型架构,只做一些小的修改,因为现在的图像是一个通道而不是三个,28x28而不是32x32。

我们将定义与之前相同的optimizer和criterion:

2、4、1 TensorBoard设置

现在我们将设置TensorBoard,从torch.utils导入tensorboard,并定义一个SummaryWriter,这是我们向TensorBoard写信息的关键对象。

?请注意,仅这一行就创建了一个runt/fashion_mnist_experiment_1文件夹

2、4、2 写入TensorBoard

现在让我们使用make_grid向我们的TensorBoard写一张图片--具体来说,就是一个网格:

现在在命令行中运行:tensorboard --logdir =

等号后边是logs所在的地址

导航到http://localhost:6006,应显示如下。

现在你知道如何使用TensorBoard了! 然而,这个例子可以在Jupyter笔记本中完成--TensorBoard真正擅长的地方是创建交互式可视化。我们接下来会介绍其中的一个,在教程结束时还会介绍几个。

2、4、3? 使用TensorBoard检查模型

TensorBoard的优势之一是它能够将复杂的模型结构可视化。让我们把我们建立的模型可视化。

现在刷新TensorBoard时,你应该看到一个 "Graphs "标签,看起来像这样。

继续前进,双击 "Net "以看到它的展开,看到构成模型的各个操作的详细视图。

TensorBoard有一个非常方便的功能,用于可视化高维数据,如低维空间中的图像数据;我们接下来会介绍这个。

2、4、4 向TensorBoard添加"Projectior"

cvtutorials.com: Projector是投影仪的意思,从高维向低维投影。

我们可以通过add_embedding方法将高维数据的低维表示可视化

现在在TensorBoard的 "Projector "选项卡中,你可以看到这100张图片--每张都是784维的--投射到三维空间。此外,这是互动的:你可以点击和拖动来旋转三维投影。最后,有几个提示方便看到可视化:在左上方选择 "color:label",以及启用 "night mode(夜间模式)",这将使图像更容易看到,因为它们的背景是白色的。

现在我们已经彻底检查了我们的数据,让我们展示一下TensorBoard如何使跟踪模型的训练和评估更加清晰,首先是训练。

2、4、5 用TensorBoard跟踪模型训练

在前面的例子中,我们只是简单地每2000次迭代打印了模型的运行损失。现在,我们将把运行损失记录在TensorBoard上,同时通过plot_classes_preds函数查看模型的预测结果。

?最后,让我们使用之前教程中相同的模型训练代码来训练模型,但每1000个批次将结果写入TensorBoard,而不是打印到控制台;这是用add_scalar函数完成的。

此外,当我们训练时,我们将生成一张图片,显示模型的预测与该批中包括的四张图片的实际结果。

现在你可以查看标量标签,看看在15,000次迭代训练中绘制的运行损失。

此外,我们可以看一下模型在整个学习过程中对任意批次的预测情况。请看 "Images "选项卡,在 "predictions vs. actuals "可视化下往下看;这告诉我们,例如,在仅仅3000次训练迭代后,模型已经能够区分视觉上不同的类别,如衬衫、运动鞋和大衣,尽管它并没有训练后期那样的置信度。

在之前的教程中,一旦模型被训练好,我们就会看每一类的准确性;在这里,我们将使用TensorBoard来绘制每一类的precision-recall曲线。

2、4、6 用TensorBoard评估训练好的模型

现在你会看到一个 "PR Curves "选项卡,其中包含了每个类别precision-recall曲线。继续往下看,你会发现在某些类别上,模型的 "area under the curve "几乎是100%,而在其他类别上,这一面积则更低。

这就是对TensorBoard和PyTorch与它的集成的介绍。当然,你可以在你的Jupyter笔记本中做TensorBoard所做的一切,但在TensorBoard中,你得到的视觉效果默认是互动的。

  人工智能 最新文章
2022吴恩达机器学习课程——第二课(神经网
第十五章 规则学习
FixMatch: Simplifying Semi-Supervised Le
数据挖掘Java——Kmeans算法的实现
大脑皮层的分割方法
【翻译】GPT-3是如何工作的
论文笔记:TEACHTEXT: CrossModal Generaliz
python从零学(六)
详解Python 3.x 导入(import)
【答读者问27】backtrader不支持最新版本的
上一篇文章      下一篇文章      查看所有文章
加:2022-05-10 11:54:11  更:2022-05-10 11:56:31 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 6:31:35-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码