Data Augmentation 这个词其实很容易误导人，因为 Augmentation 是增广的意思。数据增广，自然而然就会让人理解为：“增加了新的数据”。然而在实现的过程中，不一定是这样。 我们会给定一些随机化的参数，在每个 epoch 内生成随机增广后的图片。 比方说，每个 epoch 都按照一定的 random rule 来 crop 图片、调整色调，便是图像增广。 PyTorch 实现 这里拿 Fashion-MNIST 举例，同样我们会给出例子来证明我们的做法是行之有效的。 首先引入包： %matplotlib inline %config InlineBackend.figure_formats = ['svg'] from matplotlib import pyplot as plt import torch import torchvision from torch.utils import data 加载 FashionMNIST 数据集，并且禁用 shuffle，这样保证我们每个 epoch 图片顺序都是相同的，方便我们验证： def load_data_fashion_mnist(batch_size, dataloader_worker_count, resize=None): trans = [torchvision.transforms.ToTensor()] if resize: trans.insert(0, torchvision.transforms.Resize(resize)) trans.append(torchvision.transforms.RandomCrop((28, 28))) trans = torchvision.transforms.Compose(trans) mnist_train = torchvision.datasets.FashionMNIST( root="../data", train=True, transform=trans, download=True) mnist_test = torchvision.datasets.FashionMNIST( root="../data", train=False, transform=trans, download=True) return (data.DataLoader(mnist_train, batch_size, shuffle=False, num_workers=dataloader_worker_count), data.DataLoader(mnist_test, batch_size, shuffle=False, num_workers=dataloader_worker_count)) 辅助渲染的函数： def get_fashion_mnist_labels(labels): ''' 返回Fashion-MNIST数据集的文本标签 ''' text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat', 'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot'] return [text_labels[int(i)] for i in labels] def show_images(imgs, num_rows, num_cols, titles=None, scale=1.2): ''' 绘制图像列表 ''' figsize = (num_cols * scale, num_rows * scale) _, axes = plt.subplots(num_rows, num_cols, figsize=figsize) axes = axes.flatten() for i, (ax, img) in enumerate(zip(axes, imgs)): if torch.is_tensor(img): # 图片张量 ax.imshow(img.numpy()) else: # PIL图片 ax.imshow(img) ax.axes.get_xaxis().set_visible(False) ax.axes.get_yaxis().set_visible(False) if titles: ax.set_title(titles[i]) return axes 测试三个 epoch，看每次 epoch 第一个 batch 的数据： train_iter, test_iter = load_data_fashion_mnist(18, 4, resize=40) for _ in range(3): for i, (X, y) in enumerate(train_iter): if i == 0: show_images(X.reshape(18, 28, 28), 2, 9, titles=get_fashion_mnist_labels(y)) 输出： 可以看出有明显的不同。 本文中的 notebook：https://github.com/JeffersonQin/deep-learning/blob/master/tests/pytorch-data-augmentation.ipynb
论文链接：https://arxiv.org/abs/1506.02640 Abstract 以前都是用 Classifiers 来做 detection 现在：we frame object detection as a regression problem to spatially separated bounding boxes and associated class probabilities end-to-end FAST! Fast YOLO: 155 fps, double mAP of other real-time detectors YOLO: 45 fps Introduction Related works Deformable Parts Models (DPM): sliding window approach where the classifier is run at evenly spaced locations over the entire image R-CNN: first generate potential bounding boxes in an image then run a classifier on these proposed boxes post-processing: refine the bounding boxes, eliminate duplicate detections, and rescore the boxes based on other objects in the scene YOLO Fast, realtime simple architecture see the entire image => less background error learn generalizable representations of objects accuracy 不行, 虽然识别率高，但是定位精准度相对低 Architecture Unified Detection 描述大致的识别思路。 先划分成 $S\times S$ 方格，每个方格需要检测是否有物体的中心在这个方格内，产生 $B$ 个 bounding box 每个 bounding box 会产生五个预测量 $x, y, w, h, \text{confidence}$。注意：$x,y$ 是相对于 cell 的，$w, h$ 是相对于整个图像的。 confidence 描述这个区域有物体的置信度。confidence 的定义： $$ P(\text{Object}) \times \text{IoU}_{\text{pred}}^{\text{truth}} $$ 每个方格产生一个还会对 $C$ 个类别判断的概率，判断这个方格内是否含有类别 $C_i$ $$ P(\text{Class}_i \mid \text{Object}) $$ 测试的时候每个 bounding box 的置信概率就是 $$ P(\text{Class}_i \mid \text{Object}) \times P(\text{Object}) \times \text{IoU}_{\text{pred}}^{\text{truth}} = P(\text{Class}_i) \times \text{IoU}_{\text{pred}}^{\text{truth}} $$ Network Design 我们取 $B=2$，每个区域生成两个 bounding box，区域数量（边） $S=7$，然后分类的类别是 $C=10$。所以最后的输出 $$ S \times S \times (5B + C) = 7 \times 7 \times 30 $$ Training Pretrain ImageNet 数据集 前 20 个卷积层（去掉最后四个卷积层和两个全连接层）再加上一个平均池化层和一个全连接层 Activation 出最后一个层外，使用 Leaky ReLU $$ \phi(x) = \begin{cases} x & x > 0 \\ 0.1x & \text{otherwise} \end{cases} $$ 最后一层文章中说用的是 linear activation function 除此之外，因为最后的结果应该在 $[0,1]$ 范围之内，查到一个 stackoverflow 的问题说最后可能对 output 逐元素做了 sigmoid. https://stackoverflow.com/questions/49707542/yolo-v1-bounding-boxes-during-training-step Hyper Parameters $\text{batch size} = 64$ $\text{weight decay} = 0.0005$ $\text{momentum} = 0.9$ learning rate $10^{-3} \rightarrow 10^{-2}$ for some epochs $10^{-2}$ for $75$ epochs $10^{-3}$ for $30$ epochs $10^{-4}$ for $30$ epochs Regularization Dropout (0.5) after the first connected layers Loss Function 问题一：均方差损失函数对所有东西的权重都相同 如果一个区域不含东西，那么根据定义 confidence 直接降到 0，但是这就会造成梯度的急剧抖动。同时，现在不管是对位置的预测还是对概率的置信，所有权重都是相同的，这显然不符合目标，所有作者提出了下面两个 param: $$ \lambda_{\text{coord}} = 5, \lambda_{\text{noobj}} = .5 $$ 在算坐标预测的 loss 的时候提高权重，然后对于不含目标的划分区间降低权重。 问题二：不管 bounding box 的大小，权重都一样 解决方案：计算 $w, h$ 方根的均方损失 问题三 YOLO 对于每个区域都会产生若干个 bounding box，但是训练的时候我们只希望每个目标对应一个 bounding box。所以最后计算 loss 的时候，我们就会 assign 一个 bounding box predictor 给每个目标。这个 assign 的依据是取 $$ \operatorname{argmax}_{i, B_i \in B} \text{IoU}_{\text{truth}}^{B_i} $$ Loss 原文好像 sigma 的下标有点问题，这里做一下修正 $$ \begin{aligned} &\lambda_{\textbf{coord}} \sum_{i=1} ^ {S^2} \sum_{j=1}^{B} 1 _{ij}^{\text{obj}} \left[(x_i - \hat{x_i})^2 + (y_i - \hat{y_i})^2\right] \\ +&\lambda_{\textbf{coord}} \sum_{i=1} ^ {S^2} \sum_{j=1}^{B} 1 _{ij}^{\text{obj}} \left[ \left(\sqrt{w_i} - \sqrt{\hat{w_i}}\right)^2 + \left(\sqrt{h_i} - \sqrt{\hat{h_i}}\right)^2 \right] \\ +&\sum_{i=1} ^ {S^2} \sum_{j=1}^{B} 1 _{ij}^{\text{obj}}\left(C_i - \hat{C_i}\right)^2 \\ +&\lambda_{\textbf{noobj}}\sum_{i=1} ^ {S^2} \sum_{j=1}^{B} 1 _{ij}^{\text{noobj}}\left(C_i - \hat{C_i}\right)^2 \\ +&\sum_{i=1}^{S^2} 1_{i}^{\text{obj}} \sum_{c\in\text{classes}} (p_i(c) - \hat{p_i}(c))^2 \end{aligned} $$ where $1^{\text{obj}} _i$ denotes if object appears in cell $i$, and $1^{\text{obj}}_{ij}$ denotes that the $j$th bounding box predictor in cell $i$ is “responsible” for that prediction. 从上到下看还挺好理解的。 实践中的问题 对于每一个 cell，如果有多个东西就无解了，因为 loss 中没有说明这种情况。 一些想法 总的来说，Yolo v1 apply 了大量的 tricks... 太难训了。我自己训比原文低了 15 个点左右 ... 虽然用的是 resnet18/50。
本节主要为以下内容的笔记：https://zh-v2.d2l.ai/chapter_multilayer-perceptrons/environment.html 训练到底在做什么？ 训练其实是在最小化通过数据集得到的经验风险： $$ \mathop{\mathrm{minimize}}_f \frac{1}{n} \sum_{i=1}^n l(f(\mathbf{x}_i), y_i), $$ 其中 $l$ 是损失函数，用来度量对于给定的标签 $y_i$，预测值 $f(\mathbf {x}_i)$ 的糟糕程度。 但是我们想要得到的又是什么呢？我们想要的是最小化实际风险，也就是最小化下面这个期望： $$ E_{p(\mathbf{x}, y)} [l(f(\mathbf{x}), y)] = \int\int l(f(\mathbf{x}), y) p(\mathbf{x}, y) \;d\mathbf{x}dy $$ 然而在实践中，我们通常无法获得总体数据。 因此，经验风险最小化， 是一种实用的机器学习策略，希望能近似最小化真实风险。 协变量偏移 在不同分布偏移中，协变量偏移可能是最为广泛研究的。这里我们假设：虽然输入的分布可能随时间而改变，但标签函数（即条件分布 $P(y \mid \mathbf{x})$ ）没有改变。统计学家称之为协变量偏移 （covariate shift），因为这个问题是由于协变量（特征）分布的变化而产生的。虽然有时我们可以在不引用因果关系的情况下对分布偏移进行推断，但在我们认为 $\mathbf{x}$ 导致 $y$ 的情况下，协变量偏移是一种自然假设。 协变量偏移的例子 现在我们使用猫狗照片 $\mathbf x$ 来训练一个猫狗分类器来给出猫狗标签 $y$ 训练数据集是真实的猫狗图片 测试数据集是卡通形象的猫狗图片 虽然猫狗图片的分布 $P(\mathbf{x})$ 被改变了（测试集上图片的分布改变了），但是猫狗图片对于猫狗分类的条件分布 $P(y \mid \mathbf{x})$ 并未改变（确定了照片，分类也就确定了） 这是由于特征分布的变化导致的（协变量的变化，这里的协变量就是图片风格） 在这种情况下，是猫狗图片 $\mathbf x$ 导致猫狗分类 $y$ 的，所以我们一般可以使用协变量偏移假设 协变量偏移的纠正 见：https://zh-v2.d2l.ai/chapter_multilayer-perceptrons/environment.html#subsec-covariate-shift-correction 总结一下： 设目标分布为 $p(x)$，源分布为 $q(x)$，则根据协变量偏移假设 $$ p(y \mid \mathbf x) = q(y \mid \mathbf x) $$ 则 $$ \begin{aligned} &\int\int l(f(\mathbf{x}), y) p(\mathbf{x},y) \;d\mathbf{x}dy \\ =&\int\int l(f(\mathbf{x}), y) p(y \mid \mathbf{x})p(\mathbf{x}) \;d\mathbf{x}dy \\ =&\int\int l(f(\mathbf{x}), y) q(y \mid \mathbf{x})q(\mathbf{x})\frac{p(\mathbf{x})}{q(\mathbf{x})} \;d\mathbf{x}dy. \end{aligned} $$ 所以现在我们需要一个额外的权重 $$ \beta_i \stackrel{\mathrm{def}}{=} \frac{p(\mathbf{x}_i)}{q(\mathbf{x}_i)} $$ 来调整训练方式： $$ \mathop{\mathrm{minimize}}_f \frac{1}{n} \sum_{i=1}^n \beta_i l(f(\mathbf{x}_i), y_i) $$ 为了得到这个权重，我们可以通过训练一个二元分类器，来学习数据到底是来自于训练集还是来自于测试集。如果无法区分这两个分布，则意味着想相关的样本可能来自这两个分布中的任何一个。 另一方面，任何可以很好区分的样本都应该相应地显著增加或减少权重。 接下来是训练的方法：为了简单起见，假设我们分别从 $p(x)$ 和 $q(x)$ 两个分布中抽取相同数量的样本。 现在用标签 $z$ 表示：从 $p$ 抽取的数据为 $1$，从 $q$ 抽取的数据为 $-1$。 根据上面的假设，我们有： $$ P(z=1 \mid \mathbf{x}) = \frac{p(\mathbf{x})}{p(\mathbf{x})+q(\mathbf{x})} $$ $$ P(z=-1 \mid \mathbf{x}) = \frac{q(\mathbf{x})}{p(\mathbf{x})+q(\mathbf{x})} $$ 所以混合分布，也就是权重 $\beta$ 的分布可以由下式给出： $$ \frac{P(z=1 \mid \mathbf{x})}{P(z=-1 \mid \mathbf{x})} = \frac{p(\mathbf{x})}{q(\mathbf{x})} = \beta (\mathbf x) $$ 下一个问题就是如何训练得到 $P(z=1 \mid \mathbf{x})$ 了。我们可以使用对数回归： $$ P(z=1 \mid \mathbf{x})=\frac{1}{1+\exp(-h(\mathbf{x}))} = \sigma(h(\mathbf x)) $$ 这样的话： $$ \beta_i = \frac{1/(1 + \exp(-h(\mathbf{x}_i)))}{\exp(-h(\mathbf{x}_i))/(1 + \exp(-h(\mathbf{x}_i)))} = \exp(h(\mathbf{x}_i)) $$ 标签偏移 标签偏移（label shift）描述了与协变量偏移相反的问题。这里我们假设标签边缘概率 $P(y)$ 可以改变，但是类别条件分布 $P(\mathbf{x} \mid y)$ 在不同的领域之间保持不变。当我们认为 $y$ 导致 $\mathbf{x}$ 时，标签偏移是一个合理的假设。例如，预测患者的疾病，我们可能根据症状来判断，即使疾病的相对流行率随着时间的推移而变化。标签偏移在这里是恰当的假设，因为疾病会引起症状。在另一些情况下，标签偏移和协变量偏移假设可以同时成立。例如，当标签是确定的，即使 $y$ 导致 $\mathbf{x}$ ，协变量偏移假设也会得到满足。有趣的是，在这些情况下，使用基于标签偏移假设的方法通常是有利的。这是因为这些方法倾向于包含看起来像标签（通常是低维）的对象，而不是像输入（通常是高维的）对象。 标签偏移的例子 现在我们通过症状 $\mathbf x$ 来预测疾病 $y$ 然而，疾病随着时间的推移，疾病的流行程度发生了变化，也就是 $P(y)$ 发生了变化 但是尽管如此，一种确定的疾病导致的症状是不怎么会发生改变的，也就是 $P(\mathbf x \mid y)$ 基本保持不变 注意：这个情况下是 $y$ 决定 $\mathbf x$，所以我们就可以使用标签偏移假设 标签偏移的纠正 见：https://zh-v2.d2l.ai/chapter_multilayer-perceptrons/environment.html#id14 总结一下： 设目标分布为 $p(x)$，源分布为 $q(x)$，则标签偏移假设 $$ p(\mathbf x \mid y) = q(\mathbf x \mid y) $$ 则 $$ \begin{aligned} &\int\int l(f(\mathbf{x}), y) p(\mathbf{x},y) \;d\mathbf{x}dy \\ =&\int\int l(f(\mathbf{x}), y) p(\mathbf{x} \mid y)p(y) \;d\mathbf{x}dy \\ =&\int\int l(f(\mathbf{x}), y) q(\mathbf{x} \mid y)q(y)\frac{p(y)}{q(y)} \;d\mathbf{x}dy \end{aligned} $$ 我们同样需要一个额外的权重： $$ \beta_i \stackrel{\mathrm{def}}{=} \frac{p(y_i)}{q(y_i)} $$ 其中，$q(y)$ 很容易估计，只需要使用训练数据集进行计算即可： $$ q(y_i) = \frac {\sum_{j=1}^n 1 \{y_i = y_j\}} {n} $$ 但是 $p(y)$ 就没有这么容易了。我们需要另辟蹊径。 虽然我们不能获得真实数据，但是我们有测试数据，我们可以用测试数据来估计真实数据。令 $\mu(\hat {\mathbf {y}}) \in \mathbb {R}^k$ 为模型测试时各个标签的预测均值，即 $\mu(\hat y_i)$ 为预测标签为 $i$ 时的预测均值。那么，我们可以有： $$ \mathbf Cp(\mathbf y) = \mu (\mathbf y) $$ 其中，$\mathbf C$ 是一个 $k \times k$ 的混淆矩阵，其中 $c_{ij}$ 代表验证集中真实标签为 $j$ 但是模型预测为 $i$ 的占比。因为作为一个估计，我们将矩阵乘法按照行向量拆解来看 $\sum_{j = 1}^k c_{ij} p(y_j) = \mu (\hat y_i)$。 对于 $\mathbf C$，应该存在 $\mathbf C^{-1}$，这样我们就能得到 $p(\mathbf y)$ $$ p(\mathbf{y}) = \mathbf{C}^{-1} \mu(\hat{\mathbf{y}}) $$ 也就能得到权重 $\beta$.
Preface 这块内容比较搞脑子，这里举一个较为简单的参数复用的例子来说明。基础内容请看： https://zh-v2.d2l.ai/chapter_multilayer-perceptrons/backprop.html 回顾偏导数的链式法则 对于 $$ u = g(f_1(x_1, x_2, \cdots, x_n), f_2(x_1, x_2, \cdots, x_n), \cdots, f_m(x_1, x_2, \cdots, x_n)) $$ 而言 $$ \frac{\partial u}{\partial x_j} = \sum_{i = 1} ^ m \frac {\partial g} {\partial f_i} \frac {\partial f_i} {\partial x_j} $$ 我们不妨将链式求导写成雅可比矩阵，更加通俗易懂： $$ \left(\begin{array}{cccc} \frac{\partial u_{1}}{\partial x_{1}} & \frac{\partial u_{1}}{\partial x_{2}} & \cdots & \frac{\partial u_{1}}{\partial x_{n}} \\ \frac{\partial u_{2}}{\partial x_{1}} & \frac{\partial u_{2}}{\partial x_{2}} & \cdots & \frac{\partial u_{2}}{\partial x_{n}} \\ \cdots & \cdots & \cdots & \cdots \\ \frac{\partial u_{p}}{\partial x_{1}} & \frac{\partial u_{p}}{\partial x_{2}} & \cdots & \frac{\partial u_{p}}{\partial x_{n}} \end{array}\right)=\left(\begin{array}{cccc} \frac{\partial g_{1}}{\partial y_{1}} & \frac{\partial g_{1}}{\partial y_{2}} & \cdots & \frac{\partial g_{1}}{\partial y_{m}} \\ \frac{\partial g_{2}}{\partial y_{1}} & \frac{\partial g_{2}}{\partial y_{2}} & \cdots & \frac{\partial g_{2}}{\partial y_{m}} \\ \cdots & \cdots & \cdots & \cdots \\ \frac{\partial g_{p}}{\partial y_{1}} & \frac{\partial g_{p}}{\partial y_{2}} & \cdots & \frac{\partial g_{p}}{\partial y_{m}} \end{array}\right)\left(\begin{array}{cccc} \frac{\partial f_{1}}{\partial x_{1}} & \frac{\partial f_{1}}{\partial x_{2}} & \cdots & \frac{\partial f_{1}}{\partial x_{n}} \\ \frac{\partial f_{2}}{\partial x_{1}} & \frac{\partial f_{2}}{\partial x_{2}} & \cdots & \frac{\partial f_{2}}{\partial x_{n}} \\ \cdots & \cdots & \cdots & \cdots \\ \frac{\partial f_{m}}{\partial x_{1}} & \frac{\partial f_{m}}{\partial x_{2}} & \cdots & \frac{\partial f_{m}}{\partial x_{n}} \end{array}\right) $$ 参数复用的例子 假设我们现在有如下的简单方程 (略去了激活函数来方便验证)： $$ A_1 = W x $$ $$ Z_1 = A_1 + b_1 $$ $$ A_2 = W Z_1 $$ $$ Z_2 = A_2 + b_2 $$ 这个过程中，$W$ 被用到了两次。思考一下在自动求导的过程中如果要求 $\displaystyle \frac{\partial Z_2}{\partial W}$ 会发生什么。 我们不妨先绘制出计算图 标出每一步的导数： 我们来看 $W$： $$ \frac{\partial Z_2}{\partial W} = \frac {\partial Z_2}{\partial A_2} \left ( \frac {\partial A_2} {\partial W} + \frac {\partial A_2}{\partial Z_1} \frac {\partial Z_1}{\partial A_1} \frac {\partial A_1} {\partial W} \right ) $$ 注意：整个过程中，我们不断地在定义多元函数，才能这么做： $$ A_1 = W x = F_1(W, x) $$ $$ Z_1 = A_1 + b_1 = F_2(A_1, b_1) $$ $$ A_2 = W Z_1 = F_3(W, Z_1) $$ $$ Z_2 = A_2 + b_2 = F_4(A_2, b_2) $$ 所以我们使用偏导的链式法则救不会出错。 参数复用的好处 共享参数通常可以节省内存，并在以下方面具有特定的好处： 对于图像识别中的CNN，共享参数使网络能够在图像中的任何地方而不是仅在某个区域中查找给定的功能。 对于RNN，它在序列的各个时间步之间共享参数，因此可以很好地推广到不同序列长度的示例。 对于自动编码器，编码器和解码器共享参数。 在具有线性激活的单层自动编码器中，共享权重会在权重矩阵的不同隐藏层之间强制正交。 Reference 这张图让人醍醐灌顶： https://baike.baidu.com/item/%E9%93%BE%E5%BC%8F%E6%B3%95%E5%88%99/3314017 https://zhuanlan.zhihu.com/p/381130818 https://zh-v2.d2l.ai/chapter_deep-learning-computation/parameters.html https://zh-v2.d2l.ai/chapter_multilayer-perceptrons/backprop.html
Preface Define Network Custom Module Custom Module with Params Implement Sequential Custom Forward Function Access Parameter Access All Params Nested Network Params Parameter Initialization Share Parameters Save & Load Network Params GPU Info GPU Device Network on GPU Preface 这块内容繁多冗长，每次查教程也很繁琐，所以就直接整理成 Cheatsheet了。 代码全部出自 d2l chapter 5. Define Network import torch from torch import nn from torch.nn import functional as F net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10)) X = torch.rand(2, 20) net(X) tensor([[-0.1216, 0.0153, 0.0546, -0.0989, -0.0582, 0.1448, -0.3097, -0.0478, -0.1381, 0.0593], [-0.1315, 0.0540, 0.0157, -0.0701, -0.2307, 0.0710, -0.2731, -0.0527, -0.2170, 0.1010]], grad_fn=<AddmmBackward0>) Custom Module class MLP(nn.Module): # 用模型参数声明层。这里，我们声明两个全连接的层 def __init__(self): # 调用MLP的父类Module的构造函数来执行必要的初始化。 # 这样，在类实例化时也可以指定其他函数参数，例如模型参数params（稍后将介绍） super().__init__() self.hidden = nn.Linear(20, 256) # 隐藏层 self.out = nn.Linear(256, 10) # 输出层 # 定义模型的前向传播，即如何根据输入X返回所需的模型输出 def forward(self, X): # 注意，这里我们使用ReLU的函数版本，其在nn.functional模块中定义。 return self.out(F.relu(self.hidden(X))) net = MLP() net(X) tensor([[ 0.0097, 0.0207, 0.1453, -0.0685, 0.0505, 0.2176, 0.0180, -0.2566, 0.1506, -0.0075], [ 0.0425, -0.0261, 0.1969, 0.0842, 0.0037, 0.1542, -0.0176, -0.1798, 0.0179, -0.1200]], grad_fn=<AddmmBackward0>) Custom Module with Params PyTorch 会自动识别参数 (nn.Parameter) class MyLinear(nn.Module): def __init__(self, in_units, units): super().__init__() self.weight = nn.Parameter(torch.randn(in_units, units)) self.bias = nn.Parameter(torch.randn(units,)) def forward(self, X): linear = torch.matmul(X, self.weight.data) + self.bias.data return F.relu(linear) linear = MyLinear(5, 3) linear.weight Parameter containing: tensor([[-2.2981, -1.8825, -0.9347], [ 0.1222, -1.0374, 1.1512], [-0.2859, -0.0680, 0.9072], [ 1.2177, -0.8947, 0.6278], [ 1.4800, 0.5804, -0.9661]], requires_grad=True) Implement Sequential class MySequential(nn.Module): def __init__(self, *args): super().__init__() for idx, module in enumerate(args): # 这里，module是Module子类的一个实例。我们把它保存在'Module'类的成员 # 变量_modules中。module的类型是OrderedDict self._modules[str(idx)] = module def forward(self, X): # OrderedDict保证了按照成员添加的顺序遍历它们 for block in self._modules.values(): X = block(X) return X net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10)) net(X) tensor([[ 0.0978, 0.0979, 0.1417, -0.3019, 0.0232, -0.0246, 0.0881, -0.0908, -0.0428, -0.0388], [ 0.1624, 0.0251, -0.0085, -0.3342, 0.0645, -0.1956, 0.1111, -0.0802, -0.1252, 0.0666]], grad_fn=<AddmmBackward0>) Custom Forward Function class FixedHiddenMLP(nn.Module): def __init__(self): super().__init__() # 不计算梯度的随机权重参数。因此其在训练期间保持不变 self.rand_weight = torch.rand((20, 20), requires_grad=False) self.linear = nn.Linear(20, 20) def forward(self, X): X = self.linear(X) # 使用创建的常量参数以及relu和mm函数 X = F.relu(torch.mm(X, self.rand_weight) + 1) # 复用全连接层。这相当于两个全连接层共享参数 X = self.linear(X) # 控制流 while X.abs().sum() > 1: X /= 2 return X.sum() net = FixedHiddenMLP() net(X) tensor(-0.0431, grad_fn=<SumBackward0>) 注：关于参数复用，专门有文章讨论。 Access Parameter 对于这个网络： import torch from torch import nn net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1)) X = torch.rand(size=(2, 4)) net(X) tensor([[0.2621], [0.3079]], grad_fn=<AddmmBackward0>) 第三层的全部参数： net[2].state_dict() OrderedDict( [ ('weight', tensor([[ 0.0743, 0.1876, 0.0571, 0.3447, 0.3483, -0.2867, 0.3273, -0.1527]])), ('bias', tensor([0.1162])) ] ) 指定访问： print(type(net[2].bias)) print(net[2].bias) print(net[2].bias.data) <class 'torch.nn.parameter.Parameter'> Parameter containing: tensor([0.1162], requires_grad=True) tensor([0.1162]) Access All Params net[0].named_parameters() net.named_parameters() net.state_dict() Nested Network Params 如果网络有嵌套，或者有自定义参数 (nn.Parameter)，PyTorch 会自动识别所有参数。 def block1(): return nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 4), nn.ReLU()) def block2(): net = nn.Sequential() for i in range(4): # 在这里嵌套 net.add_module(f'block {i}', block1()) return net rgnet = nn.Sequential(block2(), nn.Linear(4, 1)) print(rgnet) Sequential( (0): Sequential( (block 0): Sequential( (0): Linear(in_features=4, out_features=8, bias=True) (1): ReLU() (2): Linear(in_features=8, out_features=4, bias=True) (3): ReLU() ) (block 1): Sequential( (0): Linear(in_features=4, out_features=8, bias=True) (1): ReLU() (2): Linear(in_features=8, out_features=4, bias=True) (3): ReLU() ) (block 2): Sequential( (0): Linear(in_features=4, out_features=8, bias=True) (1): ReLU() (2): Linear(in_features=8, out_features=4, bias=True) (3): ReLU() ) (block 3): Sequential( (0): Linear(in_features=4, out_features=8, bias=True) (1): ReLU() (2): Linear(in_features=8, out_features=4, bias=True) (3): ReLU() ) ) (1): Linear(in_features=4, out_features=1, bias=True) ) rgnet[0][1][0].bias.data tensor([ 0.3572, 0.2251, -0.3531, -0.0630, 0.4908, -0.4802, -0.3679, -0.2210]) Parameter Initialization Normal Distribution: def init_normal(m): if type(m) == nn.Linear: nn.init.normal_(m.weight, mean=0, std=0.01) nn.init.zeros_(m.bias) net.apply(init_normal) net[0].weight.data[0], net[0].bias.data[0] Constant: def init_constant(m): if type(m) == nn.Linear: nn.init.constant_(m.weight, 1) nn.init.zeros_(m.bias) net.apply(init_constant) net[0].weight.data[0], net[0].bias.data[0] Xavier and constant combination: def xavier(m): if type(m) == nn.Linear: nn.init.xavier_uniform_(m.weight) def init_42(m): if type(m) == nn.Linear: nn.init.constant_(m.weight, 42) net[0].apply(xavier) net[2].apply(init_42) print(net[0].weight.data[0]) print(net[2].weight.data) Custom Initialization: $$ \begin{aligned} w \sim \begin{cases} U(5, 10) & \text{ 可能性 } \frac{1}{4} \\ 0 & \text{ 可能性 } \frac{1}{2} \\ U(-10, -5) & \text{ 可能性 } \frac{1}{4} \end{cases} \end{aligned} $$ def my_init(m): if type(m) == nn.Linear: print("Init", *[(name, param.shape) for name, param in m.named_parameters()][0]) nn.init.uniform_(m.weight, -10, 10) m.weight.data *= m.weight.data.abs() >= 5 net.apply(my_init) Share Parameters # 我们需要给共享层一个名称，以便可以引用它的参数 shared = nn.Linear(8, 8) net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), shared, nn.ReLU(), shared, nn.ReLU(), nn.Linear(8, 1)) Save & Load Network Params torch.save(net.state_dict(), 'mlp.params') clone = MLP() clone.load_state_dict(torch.load('mlp.params')) clone.eval() GPU Info !nvidia-smi GPU Device 这里定义了两个函数： def try_gpu(i=0): #@save """如果存在，则返回gpu(i)，否则返回cpu()""" if torch.cuda.device_count() >= i + 1: return torch.device(f'cuda:{i}') return torch.device('cpu') def try_all_gpus(): #@save """返回所有可用的GPU，如果没有GPU，则返回[cpu(),]""" devices = [torch.device(f'cuda:{i}') for i in range(torch.cuda.device_count())] return devices if devices else [torch.device('cpu')] try_gpu(), try_gpu(10), try_all_gpus() (device(type='cuda', index=0), device(type='cpu'), [device(type='cuda', index=0), device(type='cuda', index=1)]) Network on GPU net = nn.Sequential(nn.Linear(3, 1)) net = net.to(device=try_gpu())
为什么要正则化？ 在探讨这个问题之前，我们需要先引入欠拟合和过拟合的概念。 左侧：欠拟合，high bias, $error_{train} \gg error_{dev}$ 右侧：过拟合，high variance, $error_{train} \rightarrow 0\%$ 在过拟合的时候我们需要引入正则化，来避免过拟合。 这里介绍两种正则化方式： 权重衰减 (weight decay) Dropout Weight Decay Weight Decay 的主要思想就是加快参数的更新幅度，也就是说增大损失函数，使得梯度下降的步幅变大。但为什么这样可以防止过拟合？根据奥卡姆剃刀法则，我们倾向于认为，参数越小，网络越不复杂，越不容易过拟合，而实践也证明了这一点。 下面的公式节选自我之前的汇报笔记： 逻辑回归: $\min_{w, b}J(w, b), w\in \mathbb R^{n_x}, b\in \mathbb R$ L2 正则化：重定义 $J(w, b)$ $$ J(w, b) = \frac{1}{m}\sum_{i = 1}^{m}\mathcal L(\hat y^{(i)}, y^{(i)}) + \frac{\lambda}{2m}||w||_2^2,~||w||_2^2 = \sum_{j = 1}^{n_x}w_j^2 = w^\mathrm T w $$ L1 正则化：重定义 $J(w, b)$ $$ J(w, b) = \frac{1}{m}\sum_{i = 1}^{m}\mathcal L(\hat y^{(i)}, y^{(i)}) + \frac{\lambda}{2m}||w||_1,~||w||_1 = \sum_{j = 1}^{n_x} |w_j| $$ 神经网络：重定义损失函数： $$ J(W^{[1]}, b^{[1]},\cdots, W^{[l]}, b^{[l]}) = \frac{1}{m}\sum_{i = 1}^m \mathcal L(\hat y^{(i)}, y^{(i)}) + \frac{\lambda}{2m}\sum_{i = 1}^m ||W^{[l]}||_\mathrm F^2 $$ $$ ||W^{[l]}||_\mathrm F^2 = \displaystyle\sum_{i = 1}^{n^{[l]}}\displaystyle\sum_{j=1}^{n^{[l-1]}}(W^{[l]}_{ij})^2 $$ 红色的项标出了正则化后迭代中增加的项： $$ dW^{[l]} = \frac{1}{m} dZ^{[l]} A^{[l-1] T} \color{red}{ + \frac {\lambda}{m}W^{[l]}} $$ $$ W^{[l]} \leftarrow W^{[l]} - \alpha\frac{1}{m} dZ^{[l]} A^{[l-1] T}\color{red}{ - \alpha \frac {\lambda}{m}W^{[l]}} $$ 下面的公式出自 d2l 教科书，能让我们更好的理解超参数 $\lambda$ 的作用： 对于线性回归的损失函数： $$ L(\mathbf{w}, b) = \frac{1}{n}\sum_{i=1}^n \frac{1}{2}\left(\mathbf{w}^\top \mathbf{x}^{(i)} + b - y^{(i)}\right)^2 $$ 我们将其重定义为： $$ J(\mathbf{w}, b) = L(\mathbf{w}, b) + \frac{\lambda}{2} \|\mathbf{w}\|^2, $$ 我们来看参数的迭代式发生了什么变化。这是原来的迭代式： $$ \begin{aligned} \mathbf{w} &\leftarrow \mathbf{w} - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_{\mathbf{w}} l^{(i)}(\mathbf{w}, b) = \mathbf{w} - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \mathbf{x}^{(i)} \left(\mathbf{w}^\top \mathbf{x}^{(i)} + b - y^{(i)}\right),\\ b &\leftarrow b - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_b l^{(i)}(\mathbf{w}, b) = b - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \left(\mathbf{w}^\top \mathbf{x}^{(i)} + b - y^{(i)}\right). \end{aligned} $$ 这是经过 L2 正则化后的迭代式： $$ \begin{aligned} \mathbf{w} & \leftarrow \left(1{\color{red}{- \eta\lambda}} \right) \mathbf{w} - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \mathbf{x}^{(i)} \left(\mathbf{w}^\top \mathbf{x}^{(i)} + b - y^{(i)}\right) \end{aligned} $$ Intuitive understanding of L2 Regularization $$ \lambda \uparrow \Longrightarrow W \downarrow \Longrightarrow \text{ some of the hidden units may loss effect} $$ $$ \lambda \uparrow \Longrightarrow W \downarrow \Longrightarrow Z = WA + b \downarrow \Longrightarrow \tanh (z) \rightarrow \text {linear} \Longrightarrow \text {simpler network} $$ $$ \lambda \uparrow \Longrightarrow \text {high variance, overfit} \rightarrow \text{just right} \rightarrow \text {high bias, underfit} $$ Weight Decay 的代码实现 如果是自己写，那么我们只需要： def l2_penalty(w): return torch.sum(w.pow(2)) / 2 l = loss(net(X), y) + lambd * l2_penalty(w) 如果使用 pytorch，我们可以更好地定义 optimizer: trainer = torch.optim.SGD([ {"params": net[0].weight,'weight_decay': wd}, {"params": net[0].bias}], lr=lr) Dropout 另一种正则化，也就是防止过拟合的方法就是 dropout。 下面的内容引用自 d2l 教课书: 在探究泛化性之前，我们先来定义一下什么是一个“好”的预测模型？ 我们期待“好”的预测模型能在未知的数据上有很好的表现： 经典泛化理论认为，为了缩小训练和测试性能之间的差距，应该以简单的模型为目标。 简单性以较小维度的形式展现， 我们在 4.4节 讨论线性模型的单项式函数时探讨了这一点。 此外，正如我们在 4.5节 中讨论权重衰减（正则化）时看到的那样， 参数的范数也代表了一种有用的简单性度量。 简单性的另一个角度是平滑性，即函数不应该对其输入的微小变化敏感。 例如，当我们对图像进行分类时，我们预计向像素添加一些随机噪声应该是基本无影响的。 1995年，克里斯托弗·毕晓普证明了 具有输入噪声的训练等价于Tikhonov正则化 [Bishop, 1995]。 这项工作用数学证实了“要求函数光滑”和“要求函数对输入的随机噪声具有适应性”之间的联系。 然后在2014年，斯里瓦斯塔瓦等人 [Srivastava et al., 2014] 就如何将毕晓普的想法应用于网络的内部层提出了一个想法： 在训练过程中，他们建议在计算后续层之前向网络的每一层注入噪声。 因为当训练一个有多层的深层网络时，注入噪声只会在输入-输出映射上增强平滑性。 这个想法被称为暂退法（dropout）。 暂退法在前向传播过程中，计算每一内部层的同时注入噪声，这已经成为训练神经网络的常用技术。 这种方法之所以被称为暂退法，因为我们从表面上看是在训练过程中丢弃（drop out）一些神经元。 在整个训练过程的每一次迭代中，标准暂退法包括在计算下一层之前将当前层中的一些节点置零。 需要说明的是，暂退法的原始论文提到了一个关于有性繁殖的类比： 神经网络过拟合与每一层都依赖于前一层激活值相关，称这种情况为“共适应性”。 作者认为，暂退法会破坏共适应性，就像有性生殖会破坏共适应的基因一样。 那么关键的挑战就是如何注入这种噪声。 一种想法是以一种无偏向（unbiased）的方式注入噪声。 这样在固定住其他层时，每一层的期望值等于没有噪音时的值。 在毕晓普的工作中，他将高斯噪声添加到线性模型的输入中。 在每次训练迭代中，他将从均值为零的分布 $\epsilon \sim \mathcal{N}(0,\sigma^2)$ 采样噪声添加到输入 $\mathbf{x}$， 从而产生扰动点 $\mathbf{x}' = \mathbf{x} + \epsilon$， 期望是 $E[\mathbf{x}'] = \mathbf{x}$。 在标准暂退法正则化中，通过按保留（未丢弃）的节点的分数进行规范化来消除每一层的偏差。 换言之，每个中间值 $h$ 以暂退概率 $p$ 由随机变量 $h'$ 替换，如下所示： $$ \begin{aligned} h' = \begin{cases} 0 & \text{ 概率为 } p \\ \frac{h}{1-p} & \text{ 其他情况} \end{cases} \end{aligned} $$ 根据此模型的设计，其期望值保持不变，即 $E[h'] = h$。 Dropout 的代码实现 纯手写： def dropout_layer(X, dropout): assert 0 <= dropout <= 1 # 在本情况中，所有元素都被丢弃 if dropout == 1: return torch.zeros_like(X) # 在本情况中，所有元素都被保留 if dropout == 0: return X mask = (torch.rand(X.shape) > dropout).float() return mask * X / (1.0 - dropout) pytorch: net = nn.Sequential(nn.Flatten(), nn.Linear(784, 256), nn.ReLU(), # 在第一个全连接层之后添加一个dropout层 nn.Dropout(dropout1), nn.Linear(256, 256), nn.ReLU(), # 在第二个全连接层之后添加一个dropout层 nn.Dropout(dropout2), nn.Linear(256, 10)) def init_weights(m): if type(m) == nn.Linear: nn.init.normal_(m.weight, std=0.01) net.apply(init_weights) 注意：对于 pytorch： net.eval() # 不启用 dropout （这两个 method 还有其他作用） net.train() # 启用 dropout Reference https://zh-v2.d2l.ai/chapter_multilayer-perceptrons/weight-decay.html https://zh-v2.d2l.ai/chapter_multilayer-perceptrons/dropout.html https://blog.csdn.net/jialibang/article/details/108991631
Softmax 运算 softmax 运算用于将实数序列映射为和为一的概率序列，换句话说，softmax 运算用于将实数值规格化，使其和为一，映射成一个合理的概率分布。 下面是 softmax 的定义： $$ \hat{\mathbf{y}} = \mathrm{softmax}(\mathbf{o})\quad \text{where}\quad \hat{y}_j = \frac{\exp(o_j)}{\sum_k \exp(o_k)} $$ 对数似然与交叉熵损失函数 这里应用 d2l 教科书上的一系列公式与语段： softmax函数给出了一个向量 $\hat{\mathbf{y}}$， 我们可以将其视为“对给定任意输入$\mathbf{x}$的每个类的条件概率”。 例如，$P(y=\text{猫} \mid \mathbf{x})$。 假设整个数据集 $\{\mathbf{X}, \mathbf{Y}\}$ 具有 $n$ 个样本， 其中索引 $i$ 的样本由特征向量 $\mathbf{x}^{(i)}$ 和独热标签向量 $\mathbf{y}^{(i)}$ 组成。 我们可以将估计值与实际值进行比较： $$ P(\mathbf{Y} \mid \mathbf{X}) = \prod_{i=1}^n P(\mathbf{y}^{(i)} \mid \mathbf{x}^{(i)}) $$ 根据最大似然估计，我们最大化 $P(\mathbf{Y} \mid \mathbf{X})$ ，相当于最小化负对数似然： $$ -\log P(\mathbf{Y} \mid \mathbf{X}) = \sum_{i=1}^n -\log P(\mathbf{y}^{(i)} \mid \mathbf{x}^{(i)}) = \sum_{i=1}^n l(\mathbf{y}^{(i)}, \hat{\mathbf{y}}^{(i)}) $$ 其中，对于任何标签 $\mathbf{y}$ 和模型预测 $\hat{\mathbf{y}}$ ，损失函数为： $$ l(\mathbf{y}, \hat{\mathbf{y}}) = - \sum_{j=1}^q y_j \log \hat{y}_j $$ 再论交叉熵损失函数 我们一定要明确一个点：交叉熵损失函数的值是一个数。我们再来看这个公式： $$ l(\mathbf{y}, \hat{\mathbf{y}}) = - \sum_{j=1}^q y_j \log \hat{y}_j $$ $\mathbf{y}$ 是一个独热编码的向量 $\hat{\mathbf{y}}$ 是经过 softmax 计算的各分量和为一的预测向量 对于所有的 $y_j$，只有一个值为 $1$，其余皆为 $0$. 交叉熵损失函数的实现 # y_hat: [vector] -> matrix # y: [scalar, represent index] -> vector # the outer dimension represent sample count def cross_entropy(y_hat, y): # 这里的 range(len(y_hat)) 的意思是对于每一个 sample 都做计算，是一个 index 集合 return - torch.log(y_hat[range(len(y_hat)), y]) y_hat = torch.tensor( [[0.2, 0.7, 0.1], [0.2, 0.7, 0.1], [0.4, 0.6, 0]]) y = [1, 1, 1] cross_entropy(y_hat, y) Out: tensor([0.3567, 0.3567, 0.5108]) 由于是批量计算，所以这里 y_hat 是矩阵，y 是向量，返回值也是一个向量。 Softmax 带来的问题 https://zh-v2.d2l.ai/chapter_linear-networks/softmax-regression-concise.html#subsec-softmax-implementation-revisited 简而言之，就是 softmax 既有可能带来指数所造成的数值过大的问题，也有可能带来除法所造成的精度丧失的问题。 为了解决第一个问题，我们可以尝试通过在计算 softmax 之前从每个参数 $o_k$ 中减去 $\max(o_k)$ : $$ \begin{aligned} \hat y_j & = \frac{\exp(o_j - \max(o_k))\exp(\max(o_k))}{\sum_k \exp(o_k - \max(o_k))\exp(\max(o_k))} \\ & = \frac{\exp(o_j - \max(o_k))}{\sum_k \exp(o_k - \max(o_k))} \end{aligned} $$ 为了解决第二个问题，我们可以将 softmax 与交叉熵损失函数结合起来一起计算。因为交叉熵损失函数带了一个对数运算，可以与指数运算相抵消： $$ \begin{aligned} \log{(\hat y_j)} & = \log\left( \frac{\exp(o_j - \max(o_k))}{\sum_k \exp(o_k - \max(o_k))}\right) \\ & = \log{(\exp(o_j - \max(o_k)))}-\log{\left( \sum_k \exp(o_k - \max(o_k)) \right)} \\ & = o_j - \max(o_k) -\log{\left( \sum_k \exp(o_k - \max(o_k)) \right)}. \end{aligned} $$ pytorch 中上述的过程 在 pytorch 中，softmax 与 cross entropy loss 是一起实现的，具体我们可以看 pytorch 的文档，其中对 nn.CrossEntropyLoss 是这样定义的： $$ \ell(x, y) = L = \{l_1,\dots,l_N\}^\top, \text{where} $$ $$ l_n = - w_{y_n} \log \frac{\exp(x_{n,y_n})}{\sum_{c=1}^C \exp(x_{n,c})} \cdot \mathbb{1}\{y_n \not= \text{ignore\_index}\} $$ 可以发现，其自带了 softmax 运算。 一般来说，我们会使用 loss = nn.CrossEntropyLoss(reduction='none') 来计算。 至于这个 reduction 参数，其实就是保留 mini-batch 每个样本的值，返回一个向量。其他的选择： $$ \ell(x, y) = \begin{cases} \sum_{n=1}^N \frac{1}{\sum_{n=1}^N w_{y_n} \cdot \mathbb{1}\{y_n \not= \text{ignore\_index}\}} l_n, & \text{if reduction} = \text{“mean";}\\ \sum_{n=1}^N l_n, & \text{if reduction} = \text{“sum".} \end{cases} $$ Reference https://zh-v2.d2l.ai/chapter_linear-networks/softmax-regression-concise.html https://pytorch.org/docs/master/generated/torch.nn.CrossEntropyLoss.html#torch.nn.CrossEntropyLoss https://github.com/pytorch/pytorch/blob/master/torch/csrc/api/include/torch/nn/modules/loss.h https://blog.csdn.net/goodxin_ie/article/details/89645358