- 2024-11-11
-
发表了主题帖:
动手学深度学习(八):计算机视觉
[个人博客]
# (一) 图像增广
对图像进行随机变化(翻转和裁剪)能够扩大训练集的规模也能减少模型对某些属性的依赖,提高泛化能力.
`torchvision.transforms`模块提供了翻转和裁剪的方法
- `RandomHorizontalFlip()`50%几率使得图像向左或向右翻转
- `RandomVerticalFlip()`50%几率使得图像向上或向下翻转
- `RandomResizedCrop((200, 200), scale=(0.1, 1), ratio=(0.5, 2))` 随机裁剪原始面积0.1到1的区域,宽高比在0.5-2,宽度和高度缩放到200像素
- `ColorJitter(brightness=0.5, contrast=0, saturation=0, hue=0)` 调整亮度、对比度、饱和度和色调. 0.5就是0-0.5
- `Compose()` 提供了将多个增广方法数组合并为一个方法的方法
# (二) 微调
有些模型需要大量的数据集进行训练(比如Fashion-MNIST图像数据集),如果实例数据不够,那么很可能过拟合.解决办法除了收集更多的数据,还可以"迁移学习"将源数据集的知识迁移到目标数据集.
步骤:
- 在源数据集上预训练神经网络模型(源模型)
- 创建一个新的神经网络模型(目标模型),复制源模型上的所有模型设计及其参数(输出层除外)
- 向目标模型添加输出层,其输出数是目标数据集中的类别数.然后随机初始化该层的模型参数.
- 训练目标模型.输出层从头训练但是其他层都是只是微调.
# (三) 目标检测和边界框
目标检测就是图像中有多个目标我们既想知道他们的类别又想知道他们在图像中的具体位置.而边界框就是确定目标位置的一种表示方法.
边界框顾名思义就是一个矩形,可以由左上角和右下角两点确定,也可以由中心点和宽高决定.如果要将边界框在图中画出来,可以将边界框表示为`matplotlib`的边界框格式然后再`axes.add_patch()`即可画出.
# (四) 锚框
目标检测算法会在输入图像中采样大量区域然后判断.以每个像素为中心生成多个缩放比和宽高比不同的边界框,这些框就是锚框.
在实践中,我们用的是缩放比$s$和宽高比$r$.选择一些缩放比$s_1,...,s_n$和宽高比$r_1,...,s_m$,由于复杂度我们可以只考虑这些锚框(共n+m-1个):
$$
(s_1,r_1),(s_1,r_2),...,(s_1,r_2),(s_1,r_m),(s_2,r_1),...,(s_n,r_1)
$$
生成锚框坐标张量的实例代码:
```python
#@save
def multibox_prior(data, sizes, ratios): # data是输入数据,sizes是锚框的大小列表,ratios是锚框的宽高比列表。
"""生成以每个像素为中心具有不同形状的锚框"""
in_height, in_width = data.shape[-2:]
device, num_sizes, num_ratios = data.device, len(sizes), len(ratios)
boxes_per_pixel = (num_sizes + num_ratios - 1)
size_tensor = torch.tensor(sizes, device=device)
ratio_tensor = torch.tensor(ratios, device=device)
# 为了将锚点移动到像素的中心,需要设置偏移量。
# 因为一个像素的高为1且宽为1,我们选择偏移我们的中心0.5
offset_h, offset_w = 0.5, 0.5
steps_h = 1.0 / in_height # 在y轴上缩放步长
steps_w = 1.0 / in_width # 在x轴上缩放步长
# 生成锚框的所有中心点
center_h = (torch.arange(in_height, device=device) + offset_h) * steps_h
center_w = (torch.arange(in_width, device=device) + offset_w) * steps_w
shift_y, shift_x = torch.meshgrid(center_h, center_w, indexing='ij')
shift_y, shift_x = shift_y.reshape(-1), shift_x.reshape(-1)
# 生成“boxes_per_pixel”个高和宽,
# 之后用于创建锚框的四角坐标(xmin,xmax,ymin,ymax)
w = torch.cat((size_tensor * torch.sqrt(ratio_tensor[0]),
sizes[0] * torch.sqrt(ratio_tensor[1:])))\
* in_height / in_width # 处理矩形输入
h = torch.cat((size_tensor / torch.sqrt(ratio_tensor[0]),
sizes[0] / torch.sqrt(ratio_tensor[1:])))
# 除以2来获得半高和半宽
anchor_manipulations = torch.stack((-w, -h, w, h)).T.repeat(
in_height * in_width, 1) / 2
# 每个中心点都将有“boxes_per_pixel”个锚框,
# 所以生成含所有锚框中心的网格,重复了“boxes_per_pixel”次
out_grid = torch.stack([shift_x, shift_y, shift_x, shift_y],
dim=1).repeat_interleave(boxes_per_pixel, dim=0)
output = out_grid + anchor_manipulations
return output.unsqueeze(0)
```
只是做一个参考,我们完全可以自己实现.
**交并比(intersection over union,loU)**
如果需要描述锚框和真实边界框之间的相似性,可以用杰卡德系数表示(两个边界框的杰卡德系数就叫**交并比**),一张图就很显然了:
![交并比](https://zh-v2.d2l.ai/_images/iou.svg)
**锚框与真实边界框匹配**
原文书上很长一段话,其实算法很简单,列一个交并比矩阵(其实就是列表):
| | $B_1$ | $B_2$ | ... | $B_{n_b}$ |
| --------- | ---------- | ---------- | --- | ------------ |
| $A_1$ | $J_{11}$ | $J_{12}$ | | $J_{1n_b}$ |
| $A_2$ | $J_{21}$ | $J_{22}$ | | $J_{2n_b}$ |
| ... | | | | |
| $A_{n_a}$ | $J_{n_a1}$ | $J_{n_a2}$ | | $J_{n_an_b}$ |
我们要做的就是对于每一个锚框分配一个最接近的真实边界框,那么显然我们可以先找这个矩阵的所有元素中大的那个元素(也就是匹配的最好的),然后找到之后就把这一列和这一行划掉(代表这个A已经和这个B匹配了),当所有B都被分配完了还剩下一些A没有分配怎么办呢?那就只能将剩下的这些A(也就是这些行中)找到最佳匹配的B,而且需要大于预定义的阈值的时候才把B分配给这些剩下的A.
> 不过我有一个疑问:为什么不直接每个A(也就是每一行)直接找最匹配的B?是为了尽可能避免重复吗?
**偏移量**
当一个锚框分配一个真实边界框的时候也就是这个锚框类别被标记和这个真实边界框的类别相同了.如果锚框没有被分配真实边界框(因为可能低于预定义的阈值),将其标记为background(背景、负类锚框)
中心坐标分别为$(x_a,y_a)(x_b,y_b)$两个框的偏移(相差)可以用以下式子表示:
$$
\left(\frac{\frac{x_b-x_a}{w_a}-\mu_x}{\sigma_x},\frac{\frac{y_b-y_a}{h_a}-\mu_y}{\sigma_y},\frac{\log\frac{w_b}{w_a}-\mu_w}{\sigma_w},\frac{\log\frac{h_b}{h_a}-\mu_h}{\sigma_h}\right)
$$
常量的默认值为$\mu_x=\mu_y=\mu_w=\mu_h=0,\sigma_x=\sigma_y=0.1 , \sigma_w=\sigma_h=0.2$.负类锚框的偏移量被标记为0.
> 偏移量的四个维度有什么含义?
**预测**
将锚框和预测偏移量作为输入,用逆偏移变换返回预测的边界框坐标.当有许多锚框时,可能会输出许多相似的具有明显重叠的预测边界框,都围绕着同一目标.为了简化输出,我们可以使用非极大值抑制(non-maximum suppression,NMS)合并属于同一目标的类似的预测边界框.非极大值抑制就是不断将置信度最高的预测边界框$B_1$作为基准,然后将所有与$B_1$的IoU超过预定阈值ϵ的非基准预测边界框移除.
代码暂时没有贴...
- 2024-11-10
-
发表了主题帖:
动手学深度学习(七):循环神经网络与现代循环神经网络
[个人博客]
# (一) 文本预处理
对一段文字进行统计预测,首先得进行处理,将字符串处理为单词、字符等词元.
步骤如下:
## 1. 读取数据集
简单地将文本的每一行读入,可以使用H.G.Well的"time_machine"数据集:
```python
#@save
d2l.DATA_HUB['time_machine'] = (d2l.DATA_URL + 'timemachine.txt',
'090b5e7e70c295757f55df93cb0a180b9691891a')
def read_time_machine(): #@save
"""将时间机器数据集加载到文本行的列表中"""
with open(d2l.download('time_machine'), 'r') as f:
lines = f.readlines()
return [re.sub('[^A-Za-z]+', ' ', line).strip().lower() for line in lines]
lines = read_time_machine()
```
## 2. 词元化
将每一行又分解为若干词语:
```python
def tokenize(lines, token='word'): #@save
"""将文本行拆分为单词或字符词元"""
if token == 'word':
return [line.split() for line in lines]
elif token == 'char':
return [list(line) for line in lines]
else:
print('错误:未知词元类型:' + token)
tokens = tokenize(lines)
```
## 3.构建字典词表
将字符映射为字典的数字索引,统计词语出现的频率来分配索引,很少出现的词语会被移除并被映射到一个未知词元“\”,另外还有一些特殊词元例如:填充词元(“\”); 序列开始词元(“\”); 序列结束词元(“\”).
```python
class Vocab: #@save
"""文本词表"""
def __init__(self, tokens=None, min_freq=0, reserved_tokens=None):
if tokens is None:
tokens = []
if reserved_tokens is None:
reserved_tokens = []
# 按出现频率排序
counter = count_corpus(tokens)
self._token_freqs = sorted(counter.items(), key=lambda x: x[1],
reverse=True)
# 未知词元的索引为0
self.idx_to_token = [''] + reserved_tokens
self.token_to_idx = {token: idx
for idx, token in enumerate(self.idx_to_token)}
for token, freq in self._token_freqs:
if freq < min_freq:
break
if token not in self.token_to_idx:
self.idx_to_token.append(token)
self.token_to_idx[token] = len(self.idx_to_token) - 1
def __len__(self):
return len(self.idx_to_token)
def __getitem__(self, tokens):
if not isinstance(tokens, (list, tuple)):
return self.token_to_idx.get(tokens, self.unk)
return [self.__getitem__(token) for token in tokens]
def to_tokens(self, indices):
if not isinstance(indices, (list, tuple)):
return self.idx_to_token[indices]
return [self.idx_to_token[index] for index in indices]
@property
def unk(self): # 未知词元的索引为0
return 0
@property
def token_freqs(self):
return self._token_freqs
def count_corpus(tokens): #@save
"""统计词元的频率"""
# 这里的tokens是1D列表或2D列表
if len(tokens) == 0 or isinstance(tokens[0], list):
# 将词元列表展平成一个列表
tokens = [token for line in tokens for token in line]
return collections.Counter(tokens)
```
# (二) 语言模型与循环神经网络
语言模型通俗的来讲是:给定一个一句话前面的部分,预测接下来最有可能的一个词是什么.对于一段文字来讲,如果模型能够理解一整段话,那就不能只把一段话拆分为几个词语单独分析,需要结合这个词语的前文以及后文(加上后文就成了双向RNN)来综合分析.
一个基本的循环神经网络如下:
![循环神经网络图](https://picx.zhimg.com/70/v2-8e0301109ac2a87ef69d79f17d6c852c_1440w.avis?source=172ae18b&biz_tag=Post)
$$
\begin{aligned}
0_{t}& =g(V\mathrm{s}_{t}) \\
&=Vf(U\mathbf{x}_t+W\mathbf{s}_{t-1}) \\
&=Vf(U\mathbf{x}_{t}+Wf(U\mathbf{x}_{t-1}+W\mathbf{s}_{t-2})) \\
&=Vf(U\mathbf{x}_t+Wf(U\mathbf{x}_{t-1}+Wf(U\mathbf{x}_{t-2}+W\mathbf{s}_{t-3}))) \\
&=Vf(U\mathbf{x}_{t}+Wf(U\mathbf{x}_{t-1}+Wf(U\mathbf{x}_{t-2}+Wf(U\mathbf{x}_{t-3}+\ldots))))
\end{aligned}
$$
# (三) 循环神经网络的实现
这里就直接看使用框架的简洁实现:
```python
num_hiddens = 256
rnn_layer = nn.RNN(len(vocab), num_hiddens)
state = torch.zeros((1, batch_size, num_hiddens)) # 初始化隐状态
X = torch.rand(size=(num_steps, batch_size, len(vocab)))
Y, state_new = rnn_layer(X, state)
#@save
class RNNModel(nn.Module):
"""循环神经网络模型"""
def __init__(self, rnn_layer, vocab_size, **kwargs):
super(RNNModel, self).__init__(**kwargs)
self.rnn = rnn_layer
self.vocab_size = vocab_size
self.num_hiddens = self.rnn.hidden_size
# 如果RNN是双向的(之后将介绍),num_directions应该是2,否则应该是1
if not self.rnn.bidirectional:
self.num_directions = 1
self.linear = nn.Linear(self.num_hiddens, self.vocab_size)
else:
self.num_directions = 2
self.linear = nn.Linear(self.num_hiddens * 2, self.vocab_size)
def forward(self, inputs, state):
X = F.one_hot(inputs.T.long(), self.vocab_size)
X = X.to(torch.float32)
Y, state = self.rnn(X, state)
# 全连接层首先将Y的形状改为(时间步数*批量大小,隐藏单元数)
# 它的输出形状是(时间步数*批量大小,词表大小)。
output = self.linear(Y.reshape((-1, Y.shape[-1])))
return output, state
def begin_state(self, device, batch_size=1):
if not isinstance(self.rnn, nn.LSTM):
# nn.GRU以张量作为隐状态
return torch.zeros((self.num_directions * self.rnn.num_layers,
batch_size, self.num_hiddens),
device=device)
else:
# nn.LSTM以元组作为隐状态
return (torch.zeros((
self.num_directions * self.rnn.num_layers,
batch_size, self.num_hiddens), device=device),
torch.zeros((
self.num_directions * self.rnn.num_layers,
batch_size, self.num_hiddens), device=device))
# 训练
device = d2l.try_gpu()
net = RNNModel(rnn_layer, vocab_size=len(vocab))
net = net.to(device)
d2l.train_ch8('time traveller', 10, net, vocab, device)
```
# (四) 现代循环神经网络
## 1.双向RNN
双向RNN在之前提到过,增加了反向的隐状态,即:
![双向RNN](https://img.picui.cn/free/2024/11/09/672e4adb5f445.png)
$$
\begin{aligned}
&o_{t} =g(V\mathrm{s}_t+V^{\prime}\mathrm{s}_t^{\prime}) \\
&\mathbf{s}_{t} =f(U\mathbf{x}_t+W\mathbf{s}_{t-1}) \\
&s_{t}^{\prime} =f(U^{\prime}\mathbf{x}_{t}+W^{\prime}\mathbf{s}_{t+1}^{\prime})
\end{aligned}
$$
但是也不能盲目将双向循环神经网络应用于任何预测,有时存在严重缺陷.
## 2.深度RNN
之前介绍的循环神经网络只有一个隐藏层,堆叠两个以上的隐藏层的时候就得到了深度循环神经网络,如图所示:
![深度RNN](https://img.picui.cn/free/2024/11/09/672e4af926128.png)
老实来讲这一章没有深入读懂,涉及到的随机过程的有些知识例如马尔可夫模型/齐普夫定律等等并没有基础,只能做一个大概的阅读分享.
-
发表了主题帖:
动手学深度学习(六):卷积神经网络与现代卷积神经网络
[个人博客]
因为在实际例子中如果想要使用全连接层,如果数据维度比较大要耗费的GPU资源很多,而卷积神经网络(CNN)则能够用较少的参数同时有平移不变性.在图像检测中划分局部区域进行预测.
# (一) 卷积神经网络概述
卷积神经网络的设计就是用于探索图像数据,下以图像为例.
将图像划分为若干个区域分别与核函数(向量)相乘得到输出向量.给出一个图就能很直观地明白卷积的过程了:
![互相关](https://zh-v2.d2l.ai/_images/correlation.svg)
具体的代码实现也很简单:
```python
class Conv2D(nn.Module):
def __init__(self, kernel_size):
super().__init__()
self.weight = nn.Parameter(torch.rand(kernel_size))
self.bias = nn.Parameter(torch.zeros(1))
def forward(self, x):
return corr2d(x, self.weight) + self.bias
```
**边缘检测**
当卷积核是一个二维张量`[1.0,-1.0]`的时候就能检测水平相邻两元素是否相同.如果是相同的才会输出0.
# (二) 卷积核学习
有时候一些复杂的卷积核我们不想去手动设计那么我们也就可以通过"学习"(参数递归)来更新卷积核.我们先构造一个卷积层,并将其卷积核初始化为随机张量。接下来,在每次迭代中,我们比较`Y`与卷积层输出的平方误差,然后计算梯度来更新卷积核。
以边缘检测为例,我们可以这样更新:
```python
# 构造一个二维卷积层,它具有1个输出通道和形状为(1,2)的卷积核
conv2d = nn.Conv2d(1,1, kernel_size=(1, 2), bias=False)
# 这个二维卷积层使用四维输入和输出格式(批量大小、通道、高度、宽度),
# 其中批量大小和通道数都为1
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))
lr = 3e-2 # 学习率
for i in range(10):
Y_hat = conv2d(X)
l = (Y_hat - Y) ** 2
conv2d.zero_grad()
l.sum().backward()
# 迭代卷积核
conv2d.weight.data[:] -= lr * conv2d.weight.grad
if (i + 1) % 2 == 0:
print(f'epoch {i+1}, loss {l.sum():.3f}')
```
# (三) 填充和步幅
![互相关](https://zh-v2.d2l.ai/_images/correlation.svg)
如图,输入是3×3但是输出是2×2.为了能让输出和输入保持同样的形状,可以在周围填充一圈0.此时输出形状与填充满足关系$\left(n_h-k_h+p_h+1\right) \times\left(n_w-k_w+p_w+1\right)$.(k是kernal,p是padding)
![填充](https://zh-v2.d2l.ai/_images/conv-pad.svg)
卷积也可以跳过中间位置每次滑动多个元素,每次滑动的元素数量称作步幅.此时输出形状与填充满足关系$(n_h-k_h+p_h+s_h)/s_h\times(n_w-k_w+p_w+s_w)/s_w$.
![步幅](https://zh-v2.d2l.ai/_images/conv-stride.svg)
# (四) 多输入多输出通道
当图像不止有一种颜色的时候就对应了有多个通道,可以用多个核函数进行分别相乘.如图:
![两个输入通道](https://zh-v2.d2l.ai/_images/conv-multi-in.svg)
实现一个多输入通道卷积函数:
```python
def corr2d_multi_in(X, K):
# 先遍历“X”和“K”的第0个维度(通道维度),再把它们加在一起
return sum(d2l.corr2d(x, k) for x, k in zip(X, K))
```
如果需要一个多个输出通道的卷积函数:
```python
# 相当于有多个核函数
K = torch.stack((K, K + 1, K + 2), 0) # torch.stack是将张量在一个维度上堆叠
def corr2d_multi_in_out(X, K):
# 迭代“K”的第0个维度,每次都对输入“X”执行互相关运算。
# 最后将所有结果都叠加在一起
return torch.stack([corr2d_multi_in(X, k) for k in K], 0)
```
此外还有1×1卷积层,通常用于调整网络层的通道数量和控制模型复杂性.
# (五) 汇聚层
和之前的卷积过程的区别是对于每一个块不是进行每个位置的元素相乘然后所有元素求和而是找出最大的那个元素(最大汇聚层)或者平均值(平均汇聚层).如图:
![最大汇聚层](https://zh-v2.d2l.ai/_images/pooling.svg)
实现方式与前面卷积类似:
```python
def pool2d(X, pool_size, mode='max'):
p_h, p_w = pool_size
Y = torch.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
if mode == 'max':
Y[i, j] = X[i: i + p_h, j: j + p_w].max()
elif mode == 'avg':
Y[i, j] = X[i: i + p_h, j: j + p_w].mean()
return Y
```
汇聚层同样可以控制填充和步幅,但是默认情况下,深度学习框架中的步幅与汇聚窗口的大小相同.也可以手动设定:
```python
pool2d = nn.MaxPool2d(3, stride=(2, 3), padding=(0, 1))
```
汇聚层使用多个通道的时候不是像卷积层一样一个卷积核与多个输入通道都进行运算而是在每个输入通道上单独运算得到一个单独的结果.
```python
X = torch.cat((X, X + 1), 1) # 也是连接但是不生成新的维度
'''
tensor([[[[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[12., 13., 14., 15.]],
[[ 1., 2., 3., 4.],
[ 5., 6., 7., 8.],
[ 9., 10., 11., 12.],
[13., 14., 15., 16.]]]])
'''
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)
'''
tensor([[[[ 5., 7.],
[13., 15.]],
[[ 6., 8.],
[14., 16.]]]])
'''
```
# (六) LeNet卷积神经网络
LeNet神经网络是最早发布的卷积神经网络之一,在计算机视觉任务中有高效性能.它有两个部分:
- 卷积编码器(两个卷积层)
- 全连接层密集层(三个全连接层)
![LeNet](https://zh-v2.d2l.ai/_images/lenet.svg)
通过API我们可以将上图简洁实现如下:
```python
net = nn.Sequential(
nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Flatten(),
nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),
nn.Linear(120, 84), nn.Sigmoid(),
nn.Linear(84, 10))
```
训练使用了GPU:
```python
#@save
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
def init_weights(m):
if type(m) == nn.Linear or type(m) == nn.Conv2d:
nn.init.xavier_uniform_(m.weight)
net.apply(init_weights)
print('training on', device)
net.to(device)
optimizer = torch.optim.SGD(net.parameters(), lr=lr) # 随机梯度下降
loss = nn.CrossEntropyLoss() # 交叉熵
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
legend=['train loss', 'train acc', 'test acc'])
timer, num_batches = d2l.Timer(), len(train_iter)
for epoch in range(num_epochs):
# 训练损失之和,训练准确率之和,样本数
metric = d2l.Accumulator(3) # 功能类似数组用于累加
net.train()
for i, (X, y) in enumerate(train_iter):
timer.start()
optimizer.zero_grad()
X, y = X.to(device), y.to(device) # 移动到GPU
y_hat = net(X)
l = loss(y_hat, y)
l.backward()
optimizer.step()
with torch.no_grad():
metric.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0])
timer.stop()
train_l = metric[0] / metric[2]
train_acc = metric[1] / metric[2]
if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
animator.add(epoch + (i + 1) / num_batches,
(train_l, train_acc, None)) # 分批次画图
test_acc = evaluate_accuracy_gpu(net, test_iter)
animator.add(epoch + 1, (None, None, test_acc))
print(f'loss {train_l:.3f}, train acc {train_acc:.3f}, '
f'test acc {test_acc:.3f}')
print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec '
f'on {str(device)}')
```
# (七) 现代卷积神经网络
## 1.深度卷积神经网络(AlexNet)
AlexNet与LeNet相似,但是AlexNet有五个卷积层、两个全连接隐藏层和一个全连接层.两者对比如下:
![两个net的对比](https://zh-v2.d2l.ai/_images/alexnet.svg)
可以看出AlexNet处理的图像像素更大,卷积通道数也更多,最后几个全连接层有4096个输出.并且AlexNet使用了ReLU激活函数比sigmoid更容易训练(LeNet没使用是因为当时还没有ReLU激活函数的提出).
使用API实现如下:
```python
net = nn.Sequential(
nn.Conv2d(1, 96, kernel_size=11, stride=4, padding=1), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
# 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
nn.Conv2d(96, 256, kernel_size=5, padding=2), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
# 使用三个连续的卷积层和较小的卷积窗口。
# 除了最后的卷积层,输出通道的数量进一步增加。
# 在前两个卷积层之后,汇聚层不用于减少输入的高度和宽度
nn.Conv2d(256, 384, kernel_size=3, padding=1), nn.ReLU(),
nn.Conv2d(384, 384, kernel_size=3, padding=1), nn.ReLU(),
nn.Conv2d(384, 256, kernel_size=3, padding=1), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Flatten(),
# 这里,全连接层的输出数量是LeNet中的好几倍。使用dropout层来减轻过拟合
nn.Linear(6400, 4096), nn.ReLU(),
nn.Dropout(p=0.5),
nn.Linear(4096, 4096), nn.ReLU(),
nn.Dropout(p=0.5),
# 最后是输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000
nn.Linear(4096, 10))
```
训练使用与LeNet相同的`train_ch6`函数.
## 2.VGG块
一个VGG块由一系列卷积层组成后面再加上汇聚层.
```python
def vgg_block(num_convs, in_channels, out_channels):
layers = []
for _ in range(num_convs):
layers.append(nn.Conv2d(in_channels, out_channels,
kernel_size=3, padding=1))
layers.append(nn.ReLU())
in_channels = out_channels
layers.append(nn.MaxPool2d(kernel_size=2,stride=2))
return nn.Sequential(*layers)
```
每一个VGG块都有参数,VGG-11网络就是5个卷积块(8个卷积层+3个全连接层).用`conv_arch = ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))`来定义输入输出通道数就能实现VGG-11如下:
```python
def vgg(conv_arch):
conv_blks = []
in_channels = 1
# 卷积层部分
for (num_convs, out_channels) in conv_arch:
conv_blks.append(vgg_block(num_convs, in_channels, out_channels))
in_channels = out_channels
return nn.Sequential(
*conv_blks, nn.Flatten(),
# 全连接层部分
nn.Linear(out_channels * 7 * 7, 4096), nn.ReLU(), nn.Dropout(0.5),
nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(0.5),
nn.Linear(4096, 10))
```
训练与AlexNet还是类似,学习率可以略高一点,VGG-11的计算量比AlexNet计算量大.
## 3.网络中的网络NiN
NiN使用由一个卷积层和多个1×1卷积层组成的块,去除了容易造成过拟合的全连接层,将它们替换为全局平均汇聚层.对比图如下:
![NiN](https://zh-v2.d2l.ai/_images/nin.svg)
NiN的块如下:
```python
def nin_block(in_channels, out_channels, kernel_size, strides, padding):
return nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size, strides, padding),
nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU())
```
NiN网络:
```python
net = nn.Sequential(
nin_block(1, 96, kernel_size=11, strides=4, padding=0),
nn.MaxPool2d(3, stride=2),
nin_block(96, 256, kernel_size=5, strides=1, padding=2),
nn.MaxPool2d(3, stride=2),
nin_block(256, 384, kernel_size=3, strides=1, padding=1),
nn.MaxPool2d(3, stride=2),
nn.Dropout(0.5),
# 标签类别数是10
nin_block(384, 10, kernel_size=3, strides=1, padding=1),
nn.AdaptiveAvgPool2d((1, 1)),
# 将四维的输出转成二维的输出,其形状为(批量大小,10)
nn.Flatten())
```
## 4.并行连结网络(GoogLeNet)
这里的GoogLe应该不是谷歌哈哈...该网络的基本卷积块为Inception块:
![Inception块](https://zh-v2.d2l.ai/_images/inception.svg)
网络架构如图:
![GoogLeNet网络](https://zh-v2.d2l.ai/_images/inception-full.svg)
实现起来与之前的类似,这里不再过多赘述.
## 5.残差网络(ResNet)
残差块(右)与正常块的对比:
![残差块](https://zh-v2.d2l.ai/_images/residual-block.svg)
残差块(residual blocks)的核心思想是每个附加层都应该更容易的包含原始函数作为其元素之一.这里加上了原始的输入.如果想要改变通道数,可以引入一个1×1卷积层与输入变换后再相加.
ResNet-18架构网络如下:
![ResNet](https://zh-v2.d2l.ai/_images/resnet18.svg)
## 6.稠密连接网络(DenseNet)
DenseNet一定程度上是ResNet的逻辑扩展.DenseNet使用了函数展开的思想,比如泰勒展开就能将原函数$f(x)$展开为1阶线性和若干高阶非线性函数的和.
而连接的时候也不是直接相加而是独立的,(右边为DenseNet):
![跨层连接](https://zh-v2.d2l.ai/_images/densenet-block.svg)
稠密网络有稠密块和过渡层.过渡层用于减小通道数,因为每个稠密块都会带来通道数的增加.
- 2024-11-05
-
发表了主题帖:
动手学深度学习(五):深度学习计算
# (一) 自定义层和块
在之前使用Pytorch的API的时候我们通常都是直接使用`net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))` ,实际上这是一个`Module`类的列表.调用`net()`其实也是`net.__call__()`.
就我看来这个意义不是很大,实际上就是将一些代码整理为了一个类更方便逻辑理解.比如`net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))`就能表示为这样一个块:
```python
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)))
```
当然还能添加其它代码使这个块的功能更丰富.
# (二) 参数管理
Pytorch提供的对模型参数的读写.
- `net[i].state_dic()`返回该层参数字典`weight`和`bias`
- `net[i].bias.data` 直接访问张量
- `net[i].weight.grad` 直接访问梯度
- `*[(name, param.shape) for name, param in net.named_parameters()] #('0.weight', torch.Size([8, 4])) ('0.bias', torch.Size([8])) ('2.weight', torch.Size([1, 8])) ('2.bias', torch.Size([1]))`
# (三) 读写文件
`torch.save(tensor,'tensor-file')`存,`torch.load('tensor-file')`读
# (四) GPU
```python
torch.cuda.device_count() # 查看GPU数量
torch.device(f'cuda:{i}') # 第i块GPU,通常能够通过参数device指定
torch.device('cpu')
Z = X.cuda(1) # 移动张量以便在同一个GPU上
```
- 2024-10-25
-
发表了主题帖:
动手学深度学习(四):多层感知机
由于数学公式渲染问题,可以移步个人博客查看[本篇文章](https://eliorfoy.github.io/2024/10/25/%E5%A4%A7%E4%B8%89%E4%B8%8A/%E5%8A%A8%E6%89%8B%E5%AD%A6%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%EF%BC%88%E5%9B%9B%EF%BC%89%EF%BC%9A%E5%A4%9A%E5%B1%82%E6%84%9F%E7%9F%A5%E6%9C%BA/)
# (一)多层感知机
## 1.多层感知机介绍
如图是一张单隐藏层的感知机,当有许多层的时候,最后一层看作线性预测器,这种架构就叫做多层感知机(multilayer perceptron 即MLP).
![单隐藏层的多层感知机](https://zh.d2l.ai/_images/mlp.svg)
再应用上激活函数$\sigma$就能让多层感知机不再是简单的线性模型从而能够表示任何仿射函数.如下所示:
$$
\begin{aligned}
\mathbf{H}&=\sigma(\mathbf{X}\mathbf{W}^{(1)}+\mathbf{b}^{(1)}),
\\\mathbf{O}&=\mathbf{H}\mathbf{W}^{(2)}+\mathbf{b}^{(2)}.
\end{aligned}
$$
## 2.常见激活函数
_激活函数_(activation function)通过计算加权和并加上偏置来确定神经元是否应该被激活, 它们将输入信号转换为输出的可微运算。
### 2.1 ReLU(Rectified linear unit)函数
$\mathrm{ReLU}(x) = max(x,0)$
图像:
![ReLU(Rectified linear unit)函数](https://zh.d2l.ai/_images/output_mlp_76f463_21_0.svg)
### 2.2 sigmoid函数
前面已经提到过该函数:$\mathrm{sigmoid}(x)=\frac{1}{1+\exp(-x)}$
图像:
![sigmoid函数](https://zh.d2l.ai/_images/output_mlp_76f463_48_0.svg)
### 2.3 tanh函数
双曲正切函数:$\tanh(x)=\frac{1-\exp(-2x)}{1+\exp(-2x)}$
图像:
![tanh函数](https://zh.d2l.ai/_images/output_mlp_76f463_78_0.svg)
# (二) 多层感知机的实现
## 1.从零开始实现
完整代码如下:
```python
import torch
from torch import nn
from d2l import torch as d2l
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
num_inputs, num_outputs, num_hiddens = 784, 10, 256
W1 = nn.Parameter(torch.randn(
num_inputs, num_hiddens, requires_grad=True) * 0.01)
b1 = nn.Parameter(torch.zeros(num_hiddens, requires_grad=True))
W2 = nn.Parameter(torch.randn(
num_hiddens, num_outputs, requires_grad=True) * 0.01)
b2 = nn.Parameter(torch.zeros(num_outputs, requires_grad=True))
params = [W1, b1, W2, b2]
def relu(X):
a = torch.zeros_like(X)
return torch.max(X, a)
loss = nn.CrossEntropyLoss(reduction='none')
# 训练
num_epochs, lr = 10, 0.1
updater = torch.optim.SGD(params, lr=lr) # 随机梯度下降
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, updater)
```
可以看出基本和之前线性神经网络从零开始实现类似,不过多了一层以及激活函数使用了自定义的relu函数.
## 2.简洁实现
完整代码如下:
```python
import torch
from torch import nn
from d2l import torch as d2l
net = nn.Sequential(nn.Flatten(),
nn.Linear(784, 256),
nn.ReLU(),
nn.Linear(256, 10))
def init_weights(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, std=0.01)
# nn.init.normal_函数将线性层的权重向量中的每个元素都初始化为一个正态随机值
net.apply(init_weights);
# 训练
batch_size, lr, num_epochs = 256, 0.1, 10
loss = nn.CrossEntropyLoss(reduction='none')
trainer = torch.optim.SGD(net.parameters(), lr=lr)
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
```
# (三) 模型选择
原文废话比较多,简而言之就是训练的模型会有训练误差和泛化误差.训练误差是在训练集上训练得到的误差,泛化误差是无穷集中误差的期望值.由此我们可以知道模型会有欠拟合和过拟合,通俗的讲欠拟合就是训练误差大,表达能力不足,不能准确预测;而过拟合是指模型在训练数据上表现很好,但在新的、未见过的数据上表现不佳.
过拟合有以下一些影响因素:
- 可调整参数的数量。当可调整参数的数量(有时称为自由度)很大时,模型往往更容易过拟合。
- 参数采用的值。当权重的取值范围较大时,模型可能更容易过拟合。
- 训练样本的数量。即使模型很简单,也很容易过拟合只包含一两个样本的数据集。而过拟合一个有数百万个样本的数据集则需要一个极其灵活的模型。
由此出现了**验证集**:将我们的数据分成三份, 除了训练和测试数据集之外,还增加一个验证数据集(validation dataset), 也叫验证集(validation set).但现实是验证数据和测试数据之间的边界模糊得令人担忧...训练数据稀缺时,使用**K折交叉验证**:原始训练数据被分成个不重叠的子集。 然后执行次模型训练和验证,每次在个子集上进行训练, 并在剩余的一个子集(在该轮中没有用于训练的子集)上进行验证。 最后,通过对次实验的结果取平均来估计训练和验证误差.
# (四) 正则化模型技术
正则化(Regularization)是机器学习中用来防止模型过拟合的一种技术。
## 1.权重衰减($L_2$正则化)
之前有提到过范数,范数能够度量向量大小.因为在很多模型中,尤其是线性模型,参数的大小可以反映模型的复杂度。较大的参数值意味着模型对输入数据的变化更敏感,这可能导致模型在训练数据上过度拟合,而在未见过的数据上表现不佳.所以最小化$L_2$范数,可以防止任何单个权重变得过大,这有助于保持模型的稳定性.
而$L_2$正则化也就是权重衰减就是在损失函数上加上一个范数作为惩罚项,将原来的训练目标最小化训练标签上的预测损失,调整为最小化预测损失和惩罚项之和.现在,如果我们的权重向量增长的太大,我们的学习算法可能会更集中于最小化权重范数.
实际上还有$L_1$正则化,那为什么选择$L_2$范数呢.一个原因是它对权重向量的大分量施加了巨大的惩罚.这使得我们的学习算法偏向于在大量特征上均匀分布权重的模型.相比之下,$L_1$惩罚会导致模型将权重集中在一小部分特征上,而将其他权重清除为零.这称为特征选择(feature selection).
于是平衡后的新的额外惩罚的损失变为:
$$
L(\mathbf{w},b)+\frac{\lambda}{2}\|\mathbf{w}\|^2
$$
$\lambda$称为正则化系数,较小的$\lambda$值对应较少约束的$\mathbf{w}$,而较大的$\lambda$值对$\mathbf{w}$的约束更大.$\lambda$除以2仍然是因为求导可以抵消平方项的$\frac{1}{2}$,选择平方范数而不是标准范数也是为了方便计算.
于是小批量随机梯度下降变为:
$$
\begin{aligned}
\mathbf{w}\leftarrow(1-\eta\lambda)\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}
$$
所以在更新$\mathbf{w}$的过程中同时也在试图将$\mathbf{w}$的大小缩到零.
### 举例演示
```python
%matplotlib inline
import torch
from torch import nn
from d2l import torch as d2l
# 初始化模型参数
def init_params():
w = torch.normal(0, 1, size=(num_inputs, 1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
return [w, b]
# 定义范数惩罚
def l2_penalty(w):
return torch.sum(w.pow(2)) / 2
def train(lambd):
w, b = init_params()
net, loss = lambda X: d2l.linreg(X, w, b), d2l.squared_loss
num_epochs, lr = 100, 0.003
animator = d2l.Animator(xlabel='epochs', ylabel='loss', yscale='log',
xlim=[5, num_epochs], legend=['train', 'test'])
for epoch in range(num_epochs):
for X, y in train_iter:
# 增加了L2范数惩罚项,
# 广播机制使l2_penalty(w)成为一个长度为batch_size的向量
l = loss(net(X), y) + lambd * l2_penalty(w)
l.sum().backward()
d2l.sgd([w, b], lr, batch_size)
if (epoch + 1) % 5 == 0:
animator.add(epoch + 1, (d2l.evaluate_loss(net, train_iter, loss),
d2l.evaluate_loss(net, test_iter, loss)))
print('w的L2范数是:', torch.norm(w).item())
```
当$\lambda$为0的时候也就是没有引入惩罚的时候得到:
![](https://zh.d2l.ai/_images/output_weight-decay_ec9cc0_81_1.svg)
当设置$\lambda$为3后得到:
![](https://zh.d2l.ai/_images/output_weight-decay_ec9cc0_96_1.svg)
通过比较就可以发现,引入$\lambda$后,虽然最后训练数据集的误差到后面降不下去了,但是测试数据集中的误差得到了下降.
**简洁实现**
```python
# weight decay作为参数输入
def train_concise(wd):
net = nn.Sequential(nn.Linear(num_inputs, 1))
for param in net.parameters():
param.data.normal_()
loss = nn.MSELoss(reduction='none')
num_epochs, lr = 100, 0.003
# 偏置参数没有衰减
trainer = torch.optim.SGD([
{"params":net[0].weight,'weight_decay': wd},
{"params":net[0].bias}] # 只为权重设置了weight_decay,所以偏置参数b(bias)不会衰减
, lr=lr)
animator = d2l.Animator(xlabel='epochs', ylabel='loss', yscale='log',
xlim=[5, num_epochs], legend=['train', 'test'])
for epoch in range(num_epochs):
for X, y in train_iter:
trainer.zero_grad()
l = loss(net(X), y)
l.mean().backward()
trainer.step()
if (epoch + 1) % 5 == 0:
animator.add(epoch + 1,
(d2l.evaluate_loss(net, train_iter, loss),
d2l.evaluate_loss(net, test_iter, loss)))
print('w的L2范数:', net[0].weight.norm().item())
```
## 2.暂退法(Dropout)
摘自原文:
> 深度网络的泛化性质令人费解,而这种泛化性质的数学基础仍然是悬而未决的研究问题。 我们鼓
> 励喜好研究理论的读者更深入地研究这个主题。 本节,我们将着重对实际工具的探究,这些工具
> 倾向于改进深层网络的泛化性。
说明还没有完美解决过拟合的问题...
我们期待“好”的预测模型能在未知的数据上有很好的表现: 经典泛化理论认为,为了缩小训练和测试性能之间的差距,应该以简单的模型为目标。权重衰减中参数的范数也代表了一种有用的简单性度量。简单性的另一个角度是平滑性,即函数不应该对其输入的微小变化敏感。如果平滑那添加一些随机噪声应该是基本无影响的.所以[Srivastava _et al._, 2014](https://zh.d2l.ai/chapter_references/zreferences.html#id155 "Srivastava, N., Hinton, G., Krizhevsky, A., Sutskever, I., & Salakhutdinov, R. (2014). Dropout: a simple way to prevent neural networks from overfitting. The Journal of Machine Learning Research, 15(1), 1929–1958.")等人就想出了在网络中注入噪声.**暂退法**就是在训练过程中随机“丢弃”(即暂时移除)网络中的一些神经元(及其连接),来减少神经元之间复杂的共适应关系,增强模型的泛化能力。
有趣的是暂退法的原始论文提到的是有性生殖的类比,这说明深度学习与生物学有极其重要的联系,更别提今年的物理和化学诺奖都落入深度学习之手...
### 举例演示
```python
import torch
from torch import nn
from d2l import torch as d2l
def dropout_layer(X, dropout):
assert 0
- 2024-10-09
-
发表了主题帖:
动手学深度学习(三):线性神经网络
**数学公式在本文中显示不全,如果想要更好的阅读体验可移步[个人博客对应文章链接](https://eliorfoy.github.io/2024/10/09/%E5%A4%A7%E4%B8%89%E4%B8%8A/%E3%80%8A%E5%8A%A8%E6%89%8B%E5%AD%A6%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%E3%80%8B%EF%BC%88%E4%B8%89%EF%BC%89/)**
# (一)线性回归
## 1.模型
线性回归假设输出与各个输入之间是线性关系,下面是一个简单的线性回归模型:
$$
\hat{y} = x_1w_1+x_2w_2+b
$$
### 2.损失函数
索引为i的样本误差:
$$\ell^{(i)}(w_1,w_2,b)=\frac{1}{2}\Big(\hat{y}^{(i)}-y^{(i)}\Big)^2$$
常数$\frac{1}{2}$使对平方项求导之后常数项系数为1.
样本误差的平均衡量模型预测的质量(衡量误差的函数成为损失函数):
$$\ell(w_1,w_2,b)=\frac1n\sum_{i=1}^n\ell^{(i)}(w_1,w_2,b)=\frac1n\sum_{i=1}^n\frac12\Big(x_1^{(i)}w_1+x_2^{(i)}w_2+b-y^{(i)}\Big)^2$$
### 3.优化算法
小批量随机梯度下降进行优化:
$$
\begin{gathered}
w_{1}\leftarrow w_{1}-\frac{\eta}{|\mathcal{B}|}\sum_{i\in\mathcal{B}}\frac{\partial\ell^{(i)}(w_{1},w_{2},b)}{\partial w_{1}}=w_{1}-\frac{\eta}{|\mathcal{B}|}\sum_{i\in\mathcal{B}}x_{1}^{(i)}\left(x_{1}^{(i)}w_{1}+x_{2}^{(i)}w_{2}+b-y^{(i)}\right) \\
w_{2}\leftarrow w_{2}-\frac{\eta}{|\mathcal{B}|}\sum_{i\in\mathcal{B}}\frac{\partial\ell^{(i)}(w_{1},w_{2},b)}{\partial w_{2}}=w_{2}-\frac{\eta}{|\mathcal{B}|}\sum_{i\in\mathcal{B}}x_{2}^{(i)}\left(x_{1}^{(i)}w_{1}+x_{2}^{(i)}w_{2}+b-y^{(i)}\right)\\
b\leftarrow b-\frac\eta{|\mathcal{B}|}\sum_{i\in\mathcal{B}}\frac{\partial\ell^{(i)}(w_{1},w_{2},b)}{\partial b}=b-\frac\eta{|\mathcal{B}|}\sum_{i\in\mathcal{B}}\left(x_{1}^{(i)}w_{1}+x_{2}^{(i)}w_{2}+b-y^{(i)}\right)
\end{gathered}$$
$\mathcal{B}$表示每个小批量中的样本数,$\eta$表示学习率,批量大小和学习率的值通常是手动预先指定(称为超参数).可以由表达式看出,就是不断迭代减去损失函数的对于三个参数的偏导数的平均.
至于为什么使用梯度下降算法而不是直接令导数为0求解,可参看[这篇回答](https://www.zhihu.com/question/20319985):
>不是所有的函数都可以根据导数求出取得0值的点的, 现实的情况可能是:
>1.可以求出导数在每个点的值, 但是直接解方程解不出来, 比如一些简单的神经网络
>2.导数没有解析解, 像一个黑匣子一样, 给定输入值, 可以返回输出值, 但是具体里面是什么情况, 搞不清楚, 工程上似乎有这种情况
>以上两种就不能直接令导数为0求解.
>牛顿迭代和梯度下降法都可以计算极值, 区别在于, 梯度下降法的算法复杂度低一些, 但是迭代次数多一些; 牛顿迭代法计算的更快(初值必须设置的很合理), 但是牛顿迭代法因为有"除法"参与(对矩阵来说就是求逆矩阵), 所以每一步迭代计算量很大. 一般会根据具体的情况取舍.
### 4.矢量表示
广义上,当数据样本为n,表达式变为:
$$\hat{\boldsymbol{y}}=\boldsymbol{X}\boldsymbol{w}+b$$
损失函数变为:
$$\ell(\boldsymbol{\theta})=\frac{1}{2n}(\boldsymbol{\hat{y}}-\boldsymbol{y})^\top(\boldsymbol{\hat{y}}-\boldsymbol{y})$$
迭代步骤变成:
$$\theta\leftarrow\theta-\frac{\eta}{|\mathcal{B}|}\sum_{i\in\mathcal{B}}\nabla_{\boldsymbol{\theta}}\ell^{(i)}(\boldsymbol{\theta})$$
$$\nabla_{\boldsymbol{\theta}}\ell^{(i)}(\boldsymbol{\theta})=\begin{bmatrix}\frac{\partial\ell^{(i)}(w_1,w_2,b)}{\partial w_1}\\\frac{\partial\ell^{(i)}(w_1,w_2,b)}{\partial w_2}\\\frac{\partial\ell^{(i)}(w_1,w_2,b)}{\partial b}\end{bmatrix}=\begin{bmatrix}x_1^{(i)}(x_1^{(i)}w_1+x_2^{(i)}w_2+b-y^{(i)})\\x_2^{(i)}(x_1^{(i)}w_1+x_2^{(i)}w_2+b-y^{(i)})\\x_1^{(i)}w_1+x_2^{(i)}w_2+b-y^{(i)}\end{bmatrix}=\begin{bmatrix}x_1^{(i)}\\x_2^{(i)}\\1\end{bmatrix}(\hat{y}^{(i)}-y^{(i)})$$
### 5.为什么选择均方损失
均方损失函数可以用于线性回归的一个原因是:假设了观测中包含噪声,其中噪声服从正态分布:
$$y=\mathbf{w}^\top\mathbf{x}+b+\epsilon$$其中,$\epsilon\sim\mathcal{N}(0,\sigma^2)$
首先复习一下概率论中的极大似然估计:
似然就是由已经发生的结果来推测产生这个结果的可能环境.
举个栗子,假设进行了n次独立随机测验,其中"状态1"发生了$n_1$次,"状态2"发生了$n_2$次(从经验和直觉出发,状态1发生的概率是$\frac{n_1}{n_1+n_2}$)定义似然函数$L(\theta)=\theta^{n_1}(1-\theta)^{n_2}$,使得似然函数最大,就可以求出$\hat{\theta}=\frac{n_1}{n_1+n_2}$.
在机器学习中使用极大似然估计的算法有朴素贝叶斯、EM算法等.利用极大似然估计建立的损失函数模型,需要进一步借助**梯度下降法**来不断的更新迭代参数,来对参数进行求解。
而在本节中,y的似然:
$$P(y\mid\mathbf{x})=\frac{1}{\sqrt{2\pi\sigma^2}}\exp\left(-\frac{1}{2\sigma^2}(y-\mathbf{w}^\top\mathbf{x}-b)^2\right)$$
求似然函数的最大(由于历史原因这里取最小):
$$-\log P(\mathbf{y}\mid\mathbf{X})=-\log (\prod_{i=1}^np(y^{(i)}|\mathbf{x}^{(i)}))=\sum_{i=1}^n \left(\frac{1}{2}\log(2\pi\sigma^2)+\frac{1}{2\sigma^2}\left(y^{(i)}-\mathbf{w}^\top\mathbf{x}^{(i)}-b\right)^2\right)$$
后一项说明在高斯噪声的假设下最小均方误差等价于对线性模型的极大似然估计.
## (二)线性回归的从零开始实现
### 1.生成数据集
```python
%matplotlib inline
import random
import torch
from d2l import torch as d2l
def synthetic_data(w, b, num_examples): #@save """生成y=Xw+b+噪声"""
X = torch.normal(0, 1, (num_examples, len(w))) # 正态分布的随机矩阵,num_examples指定样本数量,len(w)指定列数
y = torch.matmul(X, w) + b # 矩阵和向量相乘
y += torch.normal(0, 0.01, y.shape) # 添加噪声
return X, y.reshape((-1, 1))
true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)
```
### 2.读取数据集
以下是一个获取小批量数据的代码:
```python
def data_iter(batch_size, features, labels):
num_examples = len(features)
indices = list(range(num_examples))
# 这些样本是随机读取的,没有特定的顺序
random.shuffle(indices)
for i in range(0, num_examples, batch_size):
batch_indices = torch.tensor(
indices[i: min(i + batch_size, num_examples)])
yield features[batch_indices], labels[batch_indices] # 这是一个生成器,实际上深度学习框架中实现的内置迭代器要比这高效得多
batch_size = 10
for X, y in data_iter(batch_size, features, labels):
print(X, '\n', y)
break
# 将数据以10个为一组随机分配,取其中的一组
```
### 3.模型与模型参数
从均值为0、标准差为0.01的正态分布中采样随机数来初始化权重, 并将偏置初始化为0.初始化后后续更新这些参数来拟合数据.
```python
w = torch.normal(0, 0.01, size=(2,1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
```
定义模型:
```python
def linreg(X, w, b): #@save
"""线性回归模型"""
return torch.matmul(X, w) + b
```
定义损失函数:
```python
def squared_loss(y_hat, y): #@save
"""均方损失"""
return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
```
定义优化算法(小批量随机梯度下降):
```python
def sgd(params, lr, batch_size): #@save
"""小批量随机梯度下降"""
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_() # 在每次参数更新后,我们需要清除旧的梯度,以便于下一次迭代时计算新的梯度
```
### 4.训练
在每次迭代中,读取一小批量训练样本,并通过模型来获得一组预测。 计算完损失后,开始反向传播,存储每个参数的梯度。 最后,调用优化算法`sgd`来更新模型参数。在机器学习中,需要多次遍历整个训练数据集(即多个epoch),在每个迭代周期(epoch)中,使用`data_iter`函数遍历整个数据集, 并将训练数据集中所有样本都使用一次(假设样本数能够被批量大小整除),`num_epochs`和学习率`lr`都是超参数,分别设为3和0.03(设置超参数需要反复试验调整)。
```python
lr = 0.03
num_epochs = 3
net = linreg
loss = squared_loss
for epoch in range(num_epochs):
for X, y in data_iter(batch_size, features, labels):
l = loss(net(X, w, b), y) # X和y的小批量损失
# 因为l形状是(batch_size,1),而不是一个标量。l中的所有元素被加到一起,
# 并以此计算关于[w,b]的梯度
l.sum().backward() # 原地操作计算梯度存储在.grad属性中
sgd([w, b], lr, batch_size) # 使用参数的梯度更新参数
with torch.no_grad(): # 暂时禁用梯度计算
train_l = loss(net(features, w, b), labels)
print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')
```
## (三)线性回归的简洁实现
利用框架提供的一些API,能够对线性回归进行简单实现.
**首先**还是生成数据集.
**然后**读取数据集使用Pytorch提供的API进行封装:
```python
def load_array(data_arrays, batch_size, is_train=True): #@save
"""构造一个PyTorch数据迭代器"""
dataset = data.TensorDataset(*data_arrays) # TensorDataset是PyTorch中用于存储数据和标签的类
return data.DataLoader(dataset, batch_size, shuffle=is_train)
batch_size = 10
data_iter = load_array((features, labels), batch_size)
```
这里的`is_train`表示是否希望数据迭代器对象在每个迭代周期内打乱数据,与上节的data_iter不同,这里我们使用`iter`构造Python迭代器,并使用`next`从迭代器中获取第一项.
**然后**定义模型和模型参数,使用Pytorch提供的Sequential类使用层来构造模型(其实可以不使用,但后续许多模型是多层的也会用到),使用Linear类来输入全连接层:
```python
# nn是神经网络的缩写
from torch import nn
net = nn.Sequential(nn.Linear(2, 1))
```
损失函数:
```python
loss = nn.MSELoss()
```
优化算法:
```python
trainer = torch.optim.SGD(net.parameters(), lr=0.03)
```
**最后**训练:
```python
num_epochs = 3
for epoch in range(num_epochs):
for X, y in data_iter:
l = loss(net(X) ,y)
trainer.zero_grad() # 反向传播之前需要将梯度归零因为默认情况下梯度是累加的
l.backward()
trainer.step() # 更新网络参数
l = loss(net(features), labels)
print(f'epoch {epoch + 1}, loss {l:f}')
```
## (四)softmax回归
回归可以用于预测多少的问题,也可以用于分类问题.**softmax 回归**(softmax regression)其实是 logistic 回归的一般形式,logistic 回归用于二分类,而 softmax 回归用于**多分类**.
### 1.softmax简单介绍
对于输入数据$\{(x_1,y_1),(x_2,y_2),\ldots,(x_m,y_m)\}$有k个类别,即$y_{i}\in\{1,2,\ldots,k\}$,那么对于softmax回归主要估算输入数据$x_i$归属于每一类的概率,即:
$$
\begin{gathered}h_{\theta}\left(x_{i}\right)=\begin{bmatrix}p\left(y_{i}=1|x_{i};\theta\right)\\p\left(y_{i}=2|x_{i};\theta\right)\\\vdots\\p\left(y_{i}=k|x_{i};\theta\right)\end{bmatrix}=\frac{1}{\sum_{j=1}^{k}e^{\theta_{j}^{T}x_{i}}}\begin{bmatrix}e^{\theta_{1}^{T}x_{i}}\\e^{\theta_{2}^{T}x_{i}}\\\vdots\\e^{\theta_{h}^{T}x_{i}}\end{bmatrix}\end{gathered}
$$
其中$\theta_1,\theta_2,\ldots,\theta_k \in \theta$是模型的参数,乘以$\frac{1}{\sum_{j=1}^{k}e^{\theta_{j}^{T}x_{i}}}$是为了让概率位于$[0,1]$并且概率之和为1.
softmax回归的代价函数(代价函数通常是损失函数在所有训练样本上的平均值或总和):
$$L(\theta)=-\frac{1}{m}\left[\sum_{i=1}^{m}\sum_{j=1}^{k}1\left\{y_{i}=j\right\}\log\frac{e^{\theta_{j}^{T}x_{i}}}{\sum_{l=1}^{k}e^{\theta_{l}^{T}x_{i}}}\right]$$
其中$1\{\cdot\}$是示性函数,即$1\{值为真的表达式\}=1$,$1\{值为假的表达式\}=0$.
至于梯度下降求解最小化代价函数可以查看这篇文章:[softmax回归原理与实现](https://zhuanlan.zhihu.com/p/98061179#:~:text=softmax%20%E5%9B%9E%E5%BD%92(softmax%20regression)%E5%85%B6%E5%AE%9E%E6%98%AF%20logistic).
### 2.softmax回归的损失函数(交叉熵损失)
上述代价公式为什么是这样的形式?这里运用了信息论中的一个叫做交叉熵的知识.
首先我们通过softmax得到的概率向量可能是一个这样的$p=[0.279,0.119,0.359,0.014,0.008,0.002,0.011,0.145,0.036,0.025]$,但是我们实际上想要得到的$y=[0,0,1,0,0,0,0,0,0,0]$,所以我们需要去找一个函数来衡量求得的概率与真实标签的差异.
在信息论中有一个相对熵(KL散度)的概念:**同一个随机变量X 有两个单独的概率分布P(x), Q(x),可以使用KL散度来衡量这两个概率分布之间的差异.**
$$D_{KL}\left(p||q\right)=\sum_{i=1}^np\left(x_i\right)\log\left(\frac{p\left(x_i\right)}{q\left(x_i\right)}\right)$$
这个公式展开之后:
$$\begin{gathered}
D_{KL}\left(p||q\right)=\sum_{i=1}^np\left(x_i\right)\log\left(\frac{p\left(x_i\right)}{q\left(x_i\right)}\right) \\
=\sum_{i=1}^np\left(x_i\right)log\left(p\left(x_i\right)\right)-\sum_{i=1}^np\left(x_i\right)log\left(q\left(x_i\right)\right) \\
=-H\left(p\left(x\right)\right)+\left[-\sum_{i=1}^np\left(x_i\right)log\left(q\left(x_i\right)\right)\right]
\end{gathered}$$
前半部分也有定义,叫做信息熵,对于信息熵的解释是: **信息量的大小与信息发生的概率成反比。概率越大,信息量越小。概率越小,信息量越大**.用$I(x)=-log(P(x))$表示信息量,而信息熵就是信息量的期望值$H(x)=- \sum P(x_i)log(P(x_i))$.后半部分叫做交叉熵,由于前半部分是一个常数,所以要想实现判定实际的输出分布与期望的输出分布的接近程度表示就可以使用交叉熵,越接近也就是交叉熵越小.以上参考自[【损失函数系列】交叉熵做损失函数理论知识_交叉熵函数如何改为损失函数](https://blog.csdn.net/gbz3300255/article/details/106810047),写的很易懂.
### 3.softmax损失函数导数
softmax的矢量表达式:
$$
\begin{aligned}
& \mathbf{O}=\mathbf{X} \mathbf{W}+\mathbf{b} \\
& \hat{\mathbf{Y}}=\operatorname{softmax}(\mathbf{O})
\end{aligned}
$$
对于损失函数对于$o_j$的导数:
$$
\begin{aligned}
l(\mathbf{y}, \hat{\mathbf{y}})
& =-\sum_{j=1}^q y_j \log \frac{\exp \left(o_j\right)}{\sum_{k=1}^q \exp \left(o_k\right)} \\
& =\sum_{j=1}^q y_j \log \sum_{k=1}^q \exp \left(o_k\right)-\sum_{j=1}^q y_j o_j \\
& =\log \sum_{k=1}^q \exp \left(o_k\right)-\sum_{j=1}^q y_j o_j . \\
\therefore \quad \partial_{o_j} l(\mathbf{y}, \hat{\mathbf{y}})
& =\frac{\exp \left(o_j\right)}{\sum_{k=1}^q \exp \left(o_k\right)}-y_j=\operatorname{softmax}(\mathbf{o})_j-y_j .
\end{aligned}$$
从上式可以看出,softmax回归的损失函数的导数是我们softmax模型分配的概率与实际发生的情况(由独热标签向量表示)之间的差异.
## (五)图像分类数据集
### 1.获取数据
本节使用的是Fashion-MNIST数据集 ([Xiao _et al._, 2017](https://zh-v2.d2l.ai/chapter_references/zreferences.html#id189 "Xiao, H., Rasul, K., & Vollgraf, R. (2017). Fashion-mnist: a novel image dataset for benchmarking machine learning algorithms. arXiv preprint arXiv:1708.07747.")),MNIST(Modified National Institute of Standards and Technology database)数据集 ([LeCun _et al._, 1998](https://zh-v2.d2l.ai/chapter_references/zreferences.html#id90 "LeCun, Y., Bottou, L., Bengio, Y., Haffner, P., & others. (1998). Gradient-based learning applied to document recognition. Proceedings of the IEEE, 86(11), 2278–2324.")) 是图像分类中广泛使用的数据集之一,Fashion-MNIST数据集更复杂.
```python
# 使用框架内置函数下载数据集并读取到内存中
from torchvision import tansforms
trans = transforms.ToTensor()
# ToTensor实例将图像数据从PIL类变成32位浮点并除以255使得所有像素的数值均在0~1之间
import torchvision
mnist_train = torchvision.datasets.FashionMNIST(
root="../data", train=True, transform=trans, download=True)
# root表示存储路径
mnist_test = torchvision.datasets.FashionMNIST(
root="../data", train=False, transform=trans, download=True)
mnist_train[0][0].shape # torch.Size([1, 28, 28]) 因为是灰度数据所以通道数为1
```
训练数据集有6000张,测试数据集有1000张。测试数据集不用于训练用于评估模型性能,另外这些图像也不以一般格式存储所以你是不能直接打开的,使用的可能是IDX文件格式.
### 2.显示数据(图像)
```python
# 可视化样本
def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): #@save
'''
绘制图像列表
Parameters:
imgs:图像列表
num_rows:要显示图像的行数
num_cols:要显示的图像的列数
title:为每个图像设置标题
scale:缩放因子
'''
figsize = (num_cols * scale, num_rows * scale)
_, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize)
# matplotlib.pyplot库创建一个图形和一组子图轴
axes = axes.flatten() # 展平,类似于矩阵变成一个列表数组,axes是axis(坐标轴)的复数
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)
# 隐藏每个子图的x轴和y轴
if titles:
ax.set_title(titles)
return axes
# 定义一个函数显示每个图像的title
def get_fashion_mnist_labels(labels): #@save
"""返回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]
# 显示图像示例
from torch.utils import data
X, y = next(iter(data.DataLoader(mnist_train, batch_size=18)))
# X是一个批次的图像数据,它通常是一个四维张量,形状为 [batch_size, channels, height, width],y是标签序号
show_images(X.reshape(18, 28, 28), 2, 9, titles=get_fashion_mnist_labels(y));
# 将X维度压缩为三维以便适应show_images函数的要求
```
### 3.读取小批量数据
这里直接使用了内置的数据迭代器不是自己写读取函数.
```python
batch_size = 256
def get_dataloader_workers(): #@save
"""使用4个进程来读取数据"""
return 4
train_iter = data.DataLoader(mnist_train, batch_size, shuffle=True,
num_workers=get_dataloader_workers())
# shuffle就是表示是否打乱样本读取小批量(shuffle有洗牌的意思)
```
### 4.获取数据并读取
其实就是一个整合.
```python
#这个函数还能通过resize参数调整图像大小
def load_data_fashion_mnist(batch_size, resize=None): #@save
"""下载Fashion-MNIST数据集,然后将其加载到内存中"""
trans = [transforms.ToTensor()]
if resize:
trans.insert(0, transforms.Resize(resize))
# transforms.Resize(resize) 将被添加到trans列表的最前面
trans = 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=True,
num_workers=get_dataloader_workers()),
data.DataLoader(mnist_test, batch_size, shuffle=False,
num_workers=get_dataloader_workers()))
```
## (六)softmax回归从零开始实现
### 1.初始化和定义softmax
获取数据:
```python
from IPython import display
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
```
因为图像是$28 \times 28$的,可以看做是一个长度为784的向量,与线性回归一样使用正态分布初始化权重$\mathrm{W}$,偏置初始化为0.
```python
num_inputs = 784
num_outputs = 10
W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
b = torch.zeros(num_outputs, requires_grad=True)
```
回顾一下sum函数,keepdim参数为True的时候能够保持原始张量轴数:
```python
X = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
X.sum(0, keepdim=True), X.sum(1, keepdim=True)
# 0是同一列求和
# 会是以下结果
# (tensor([[5., 7., 9.]]),
# tensor([[ 6.],
# [15.]]))
```
定义softmax函数如下:
```python
def softmax(X):
X_exp = torch.exp(X) # 求幂
partition = X_exp.sum(1, keepdim=True) # 求和
return X_exp / partition # 这里应用了广播机制
```
示例:
```python
X = torch.normal(0, 1, (2, 5))
X_prob = softmax(X)
X_prob, X_prob.sum(1)
# (tensor([[0.1686, 0.4055, 0.0849, 0.1064, 0.2347],
# [0.0217, 0.2652, 0.6354, 0.0457, 0.0321]]),
# tensor([1.0000, 1.0000]))
```
注意这里代码没有考虑到数值上溢或下溢,这里算不够完善.
### 2.定义模型
```python
def net(X):
return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)
```
### 3.定义损失函数和精度
```python
def cross_entropy(y_hat, y):
return - torch.log(y_hat[range(len(y_hat)), y])
# 这里的len(y_hat)返回的是行数
# 示例
y = torch.tensor([0, 2])
y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
# y_hat[[0, 1], y] pytorch提供的取矩阵中元素的方法
cross_entropy(y_hat, y)
# tensor([2.3026, 0.6931])
```
**分类精度**:正确预测数量与总预测数量之比.也定义一个函数:
```python
def accuracy(y_hat, y): #@save
"""计算预测正确的数组量"""
if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
y_hat = y_hat.argmax(axis=1)
# 沿着第一维(行)中找到最大的索引值
cmp = y_hat.type(y.dtype) == y
# y_hat的数据类型转换为y的数据类型得到bool数组
return float(cmp.type(y.dtype).sum())
# 返回预测正确样本数量,bool类型转换了一下可计算类型
```
评估模型的精度有框架函数定义:
```python
# 一个实用类
class Accumulator: #@save
"""在n个变量上累加"""
def __init__(self, n):
self.data = [0.0] * n
def add(self, *args):
self.data = [a + float(b) for a, b in zip(self.data, args)]
def reset(self):
self.data = [0.0] * len(self.data)
def __getitem__(self, idx):
return self.data[idx]
def evaluate_accuracy(net, data_iter): #@save
"""计算在指定数据集上模型的精度"""
if isinstance(net, torch.nn.Module):
net.eval()
# 将模型设置为评估模式,在深度学习中,某些层的行为在训练和评估(或测试)阶段是不同的
metric = Accumulator(2) # 正确预测数、预测总数
with torch.no_grad():
for X, y in data_iter:
metric.add(accuracy(net(X), y), y.numel()) # y.numel()返回y中元素的总数
return metric[0] / metric[1]
```
### 4.训练
```python
def train_epoch_ch3(net, train_iter, loss, updater): #@save
# updater是更新模型参数的常用函数,可以是对sgd函数(随机)
"""训练模型一个迭代周期(定义见第3章)"""
# 将模型设置为训练模式
if isinstance(net, torch.nn.Module):
net.train()
# 训练损失总和、训练准确度总和、样本数
metric = Accumulator(3)
for X, y in train_iter:
# 计算梯度并更新参数
y_hat = net(X)
l = loss(y_hat, y)
if isinstance(updater, torch.optim.Optimizer):
# 使用PyTorch内置的优化器和损失函数
updater.zero_grad()
l.mean().backward()
updater.step()
else:
# 使用定制的优化器和损失函数
l.sum().backward()
updater(X.shape[0])
metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
# 返回平均训练损失和训练精度
return metric[0] / metric[2], metric[1] / metric[2]
```
一个工具类Animator在动画中绘制数据:
```python
class Animator: #@save
"""在动画中绘制数据"""
def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None,
ylim=None, xscale='linear', yscale='linear',
fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1,
figsize=(3.5, 2.5)):
# 增量地绘制多条线
if legend is None:
legend = []
d2l.use_svg_display()
self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize)
if nrows * ncols == 1:
self.axes = [self.axes, ]
# 使用lambda函数捕获参数
self.config_axes = lambda: d2l.set_axes(
self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
self.X, self.Y, self.fmts = None, None, fmts
def add(self, x, y):
# 向图表中添加多个数据点
if not hasattr(y, "__len__"):
y = [y]
n = len(y)
if not hasattr(x, "__len__"):
x = [x] * n
if not self.X:
self.X = [[] for _ in range(n)]
if not self.Y:
self.Y = [[] for _ in range(n)]
for i, (a, b) in enumerate(zip(x, y)):
if a is not None and b is not None:
self.X.append(a)
self.Y.append(b)
self.axes[0].cla()
for x, y, fmt in zip(self.X, self.Y, self.fmts):
self.axes[0].plot(x, y, fmt)
self.config_axes()
display.display(self.fig)
display.clear_output(wait=True)
```
多次迭代:
```python
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): #@save
"""训练模型(定义见第3章)"""
animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],
legend=['train loss', 'train acc', 'test acc'])
for epoch in range(num_epochs):
train_metrics = train_epoch_ch3(net, train_iter, loss, updater)
test_acc = evaluate_accuracy(net, test_iter) # 测试模型
animator.add(epoch + 1, train_metrics + (test_acc,))
train_loss, train_acc = train_metrics
assert train_loss < 0.5, train_loss
# 断言,如果训练损失不小于0.5会排除异常并显示train_loss
assert train_acc 0.7, train_acc
assert test_acc 0.7, test_acc
```
如果使用自己的updater,以下是一个实例:
```python
lr = 0.1
def updater(batch_size):
return d2l.sgd([W, b], lr, batch_size)
num_epochs = 10
# 十个迭代周期
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater)
```
得到结果大致如图:
![参数图](https://zh.d2l.ai/_images/output_softmax-regression-scratch_a48321_222_0.svg)
### 5.预测
```python
def predict_ch3(net, test_iter, n=6): #@save
"""预测标签(定义见第3章)"""
for X, y in test_iter:
break
trues = d2l.get_fashion_mnist_labels(y)
preds = d2l.get_fashion_mnist_labels(net(X).argmax(axis=1))
titles = [true +'\n' + pred for true, pred in zip(trues, preds)]
d2l.show_images(
X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n])
predict_ch3(net, test_iter)
```
结果大致如图:
![模型预测](https://zh.d2l.ai/_images/output_softmax-regression-scratch_a48321_237_0.svg)
## (七)softmax简洁实现
基于框架能够简洁实现.
```python
import torch
from torch import nn
from d2l import torch as d2l
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
# PyTorch不会隐式地调整输入的形状。因此,
# 我们在线性层前定义了展平层(flatten),来调整网络输入的形状
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))
def init_weights(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, std=0.01)
# 正态分布来初始化权重,0.01是标准差
net.apply(init_weights);
```
在之前提到过上溢和下溢的问题,在框架中已经解决好了.这里利用的是如果每个常数$o_k$减去一个相同的常数那么得到的softmax返回值不会变,那么我们可以让$o_j-max(o_k)$就能尽量避免上溢.
$$\begin{aligned}
\hat{y}& =\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}$$
而避免下溢(主要是$log\left(\exp(o_j-\max(o_k))\right)$的溢出)就可以:
$$\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\left(\exp(o_j-\max(o_k))\right)-\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}$$
```python
loss = nn.CrossEntropyLoss(reduction='none')
# 交叉熵损失层,reduction='none'表示不进行任何聚合操作,返回每个样本的损失
trainer = torch.optim.SGD(net.parameters(), lr=0.1) # 随机梯度下降
```
最后训练:
```python
num_epochs = 10
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
```
- 2024-09-21
-
回复了主题帖:
动手学深度学习(一):预备知识
nmg 发表于 2024-9-6 14:49
说的不支持LaTex数学公式渲染问题已经反馈了
数学基础是不是可以边用边看,一下子看到这么多 ...
嗯,需要的数学知识感觉挺杂,这几天一直都在补数学知识,不然不知道代码是怎么来的,数值分析一些科目大学都没学过有些概念也不是很清楚
-
回复了主题帖:
动手学深度学习(二):数学基础
本帖最后由 EliorFoy 于 2024-9-21 22:18 编辑
这是第二篇,标题打错了抱歉
【新提醒】动手学深度学习(一) - 嵌入式系统 - 电子工程世界-论坛 (eeworld.com.cn)
这是第一篇
-
发表了主题帖:
动手学深度学习(二):数学基础
# 相关知识回顾
## 1.线性代数
①生成子空间:向量的线性组合 $\sum_{i}c_i\vec{v}_i$
②列空间:A的列向量的生成子空间$\rightarrow$列空间/值域
如果$Ax=b$对$\forall b \in R^{m} \exists 解,则列空间构成R^m$
比如:
$$
\begin{bmatrix}
1 &2 \\
3 & 4
\end{bmatrix}\cdot
\begin{bmatrix}
x \\
y
\end{bmatrix}
$$
就是构成了一个列空间,$x、y$任取就对应不同的结果,这个结果构成一个集合
③列空间构成$R^m \Rightarrow$ A至少m列即$n \ge m$
因为:假设 $A \in R^{3 \times 2}$ $b$是三维的而$x$只有两维,只能描绘$R^3$空间中的平面
④一个列向量线性相关的方阵成为奇异的
⑥矩阵可逆必为方阵(实际上$m \times n$的矩阵也存在$A$使得$A \cdot B = I_m$但那是后续的伪逆,只有在即使左逆又是右逆的情况下才叫可逆,同时可以推出可逆阵是方阵
⑦范数(norm):衡量从原点到点x的距离
$\left\|\boldsymbol{x}\right\|_p=\left(\sum_i|x_i|^p\right)^{\frac1p}$
$p=2$ 时 $L^2$为欧几里得范数 $L^2= \left\|\boldsymbol{x}\right\|_2= x \cdot x^\top$(点积)
$p=1$ 时为 $L^1$ ,当机器学习问题中零元素和非零元素的差异非常重要时就会使用该范数, $L^1$ 也替代 $L^0$ 经常作为表示非零元素数目的替代函数
$L^\infty$范数(最大范数)表示向量中具有最大幅值的元素的绝对值。即$L^ \infty = \max_i|x_i|$
Frobenius 范数: $\left\|A\right\|_F=\sqrt{\sum_{i, j}A_{i, j}^2},$ 衡量矩阵的大小
⑧对角矩阵: $i \ne j \quad D_{i,j} = 0$
$diag(v)$表示对角元素由向量$v$中元素给定的对角方阵
$diag(v) \cdot x = v \odot x$
⑨标准正交:n个向量不仅相互正交并且范数都为1
正交矩阵:行、列向量分别标准正交,即 $A \cdot A^\top = A^\top \cdot A =I$ 也即 $A^\top = A^{-1}$
⑩特征分解
特征向量: 对于非零向量$v$,方阵$A$有
$$
A \cdot v = \lambda v
$$
标量$\lambda$为特征向量对应的特征值(同理有定义左特征向量$v^\top A=\lambda A$)
⑪由于特征向量可以缩放,所以一般只考虑单位特征向量
n个线性无关的特征向量$\{v^{(1)},...v^{(n)}\}$
对应的特征值$\{\lambda_1,...\lambda_n\}$
A 的特征值分解: $Vdiag(\lambda)V^{-1}$
不是每一个矩阵都可以分解成特征值和特征向量
在某些特殊情况下特征分解存在但会涉及复数
而每个**实对称矩阵**都可以分解成实特征向量和实特征值
$$
A = Q \Lambda Q^{-1}
$$
但特征分解可能不唯一
矩阵的作用其实是让的不同向量空间里的向量进行拉伸
- 矩阵是奇异的当且仅当含有零特征值
正定: 所有特征值都是正数
半正定: 所有特征值都是非负数
同理还有负定和半负定
⑫ $f(x) = x^\top Ax$ $x$ 为某个特征向量时 $f$ 返回对应的特征值 (限制 $\left \|x \right\|_2=1$, 见花书 P 39)
⑬奇异值分解 (相比于特征分解应用更广泛, 因为不仅限于方阵)
每一个实数矩阵都有一个奇异值分解, 且非方阵只能用奇异值分解
分解:
$$
A = UDV^\top
$$
$A: m\times m$ 为正交矩阵, 列向量为左奇异向量
$D: m\times n$ 为对角矩阵, 对角元素为矩阵 A 的奇异值
$V^\top : n \times n$ 为正交矩阵, 列向量为右奇异向量
\*$\begin{cases}AA^\top\text{的特征向量即为左奇异向量}\\A^\top A\text{的特征向量即为右奇异向量}\\A\text{的非零奇异向量是}AA^\top/A^\top A\text{特征值的平方根}&\end{cases}$
⑭Moorse-Penrose 伪逆 (非方阵左逆)
A 的伪逆: $A^+ = \lim_{\alpha \to \infty}(A^\top A +\alpha I)^{-1}A^\top$
计算伪逆: $A^+ = VD^+U^\top$ $V、D、U$即为奇异分解后的是哪个矩阵
$D^\top$即为$D$非零元素取倒数后转置
$\begin{cases}mn时 \quad 可能没有解,伪逆得到的x使\left \| Ax-y \right \|_2最小\end{cases}$
⑮迹运算 $Tr(A) = \sum_{i}A_{ij}$
$\left \| A \right \|_F = \sqrt{Tr(AA^T)}$
$Tr(ABC) = Tr(CBA) = Tr(BCA)$
更一般地有:$Tr(\prod_{i}^nF^{(i)}) = Tr(F^{(n)} \prod_{i}^{n-1}F^{(i)})$
对于标量 $a = Tr(a)$
⑯行列式:绝对值衡量矩阵乘法后空间扩大或者缩小多少(如果为0则沿某一维完全收缩,比较Jacobian矩阵)
行列式的结果等于矩阵的各个特征值的乘积
对于满秩矩阵,一组特征向量可以当作一组坐标系的基
分别伸缩$\lambda_i$倍 $\therefore 空间伸缩\prod_{i}^n \lambda_i = |A|$
(表示坐标旋转的矩阵一般人两个特征值共轭的复数,复平面上表示的旋转角度的绝对值即为坐标旋转角度)
## 2.概率与信息论
①Bernoulli分布
$P(x=1)= \phi \quad P(x=0) = 1- \phi$
②Multinoulli分布(范畴分布)
具有k个不同状态的单个离散随机变量上的分布
③高斯分布(正态分布)
$\mathcal{N}(x,\mu,\sigma^2)=\sqrt{\frac{1}{2\pi \sigma^2}}e^{-\frac{1}{2\sigma^2}(x-\mu)^2}$
多维正态分布:
$\mathcal{N} = \sqrt{\frac{1}{(2\pi)^ndet(\Sigma)}}e^{-\frac{1}{2}(x-\mu)^\top\Sigma^{-1}(x-\mu)}$
其中$\mu$仍然是均值,不过是向量值;$\Sigma$是协方差矩阵,可以用$\beta$替代$\Sigma^{-1}$
更简单版本:各向同性高斯分布(协方差阵是标量$\times$单位阵)
④指数分布
$p(x;\lambda)=\lambda \mathbf{1}_{x\ge0}e^{-\lambda x}$
Laplace分布: $Laplace(x;\mu;\gamma)=\frac1{2\gamma}e^{-\frac{|x-\mu|}{\gamma}}$
可以在任意一点$\mu$设置概率质量的峰值
⑤Dirac分布 $p(x) = \delta(x)$ 集中在一点
经验分布 $\hat{p}(x) = \frac1m \sum_{i}^m{\delta(x-x^{(i)})}$
⑥常用函数
logistic sigmoid函数 $\sigma(x) = \frac1{1+e^{-x}}$ $\rightarrow$ 产生Bernoulli的$\phi$
![[logistic函数.png]]
softplus函数 $\zeta(x)=log(1+e^x)$ $\rightarrow$ 产生正态分布的$\beta$和$\sigma$
性质:
- $\sigma(x) = \frac{e^x}{e^x+e^0}$
- $\frac{d}{dx}\sigma(x)=\sigma(x)(1-\sigma(x))$
- $log(\sigma(x)) = -\zeta(-x)$
- $\frac{d}{dx}\zeta(x) = \sigma(x)$
- $\forall x \in (0,1) \quad \sigma^{-1}(x) = log(\frac1{1-x})$
- $\forall x \gt 0 \quad \zeta^{-1}(x) = log(e^x-1)$
- $\zeta(x) = \int_{-\infty}^{x}\sigma(y) \mathrm{d}y$
- $\zeta(x) - \zeta(-x) = x$
**信息论部分暂略...**
## 3.数值计算
①条件数:函数对于输入的微小变化而变化的快慢程度
$f(x) = A^{-1}x \quad A \in R^{n \times m}$ 具有特征值分解时
条件数$max_{ij}|\frac{\lambda_i}{\lambda_j}|$ 最大与最小特征值模之比
② Jacobian矩阵: $f : R^m \rightarrow R^n \quad J_{ij} = \frac{\partial}{\partial x}f(x)_i$
Hessian矩阵: $H(f)(x)_{i,j} = \frac{\partial^2}{\partial x_i \partial x_j}f(x)$ 等价于梯度的Jacobian矩阵
仅使用梯度信息的优化算法 $\Rightarrow$ 一阶优化算法
使用Hessian矩阵的优化算法 $\Rightarrow$ 二阶最优化算法
还是没找到渲染方式,可以看https://eliorfoy.github.io/2024/09/20/%E5%A4%A7%E4%B8%89%E4%B8%8A/%E3%80%8A%E5%8A%A8%E6%89%8B%E5%AD%A6%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%E3%80%8B%EF%BC%88%E4%BA%8C%EF%BC%89/
这篇渲染好的文章
- 2024-09-03
-
发表了主题帖:
动手学深度学习(一):预备知识
# 第一讲 杂谈+学前准备
## 1. 关于深度学习
本人是一名大学生,很不幸运在大学之前对深度学习一无所知。自从ChatGPT爆火全球,我才逐渐关注到原来“智能”与深度学习息息相关。本书《动手学深度学习》其实有另一套理论版《DeepLearning》(书的封面是一片花海故被称作“花书”),也是一套很经典的教程,但是由于整篇比较注重数学的推算可以说是很难啃;而本书由书名就可以知道案例比较丰富,“动手学”才能让抽象的变形象。本书第一章没有很多内容,大致是介绍一下深度学习到底是什么。个人觉得重要的是知道“监督学习”、“无监督学习”、“与环境互动”以及“强化学习”这些概念。按照我的理解,监督学习是需要为数据打上标签,自发学习就是无监督学习,与环境互动中有反馈是强化学习。
## 2. 环境配置
本书配备有完整的[电子教程](https://zh-v2.d2l.ai/)(其实电子教程已经涵盖了实体书的所有内容,所以对于想要学习DL的同志,这些资料都是开源的)。由于我之前有学过Python,对Python的虚拟环境算是比较熟悉,我没有使用书上的教程进行环境配置,书上的Python环境使用的是Miniconda,Miniconda是一个轻量级的Python发行版本,创建和管理Python环境比较方便所以应用于数据科学方面比较广泛。
### 2.1 安装Jupyter
我个人使用的是Python3.12,可以手动创建venv虚拟环境然后执行`pip install jupyter`就能安装jupyter了,至于[jupyter是什么](https://blog.csdn.net/franklfeng/article/details/117562667)可以自己去搜一下就明白为什么有这个东西了。jupyter notebook有插件可以安装,但是似乎在7.x版本的notebook中不再支持`nbextensions`插件,需要手动把notebook的版本降为6.x。插件不是必要的,安装完成后输入`jupyter notebook`即可以启动webui界面。
### 2.2 安装CUDA、cuDNN和Pytorch
深度学习一般是矩阵计算,而GPU比CPU更适合做这些计算,要调用GPU就需要安装驱动,NVIDIA系列的显卡有CUDA工具包方便我们调用,而cuDNN 是 NVIDIA 的 CUDA 深度神经网络库,可以显著提高训练速度和推断速度,需要匹配对应的cuda版本。而安装Pytorch框架也需要匹配对应的cuda版本,等我安装pytorch的时候发现我装的cuda版本是12.6而pytorch匹配的最新版本是12.4,所以在此告诉大家安装的时候一定要看清所有匹配的版本再安装。CPU也不是不能够用...等到算不够的情况下再换cuda吧。
### 2.3 云端环境
对于没有显卡的小伙伴,网上有很多[免费的GPU资源(转载)](https://www.bilibili.com/read/cv20275242/)可以使用。
# 第二讲 预备知识
本讲主要介绍的就是一些数学基础以及Python常用操作。
## 1.数据操作
```python
import torch # 后续操作几乎都用此库
x = torch.arange(12) # 0-11的张量
len(x) # 几行
x.shape # 几行几列
x.numel() # 总个数
x.reshape(3,4) # 变成三行四列,reshape(-1,4)输入-1会自动推算
torch.zeros(2,3,4)
torch.ones(2,3,4)
torch.randn(3,4) # 分别创建均为0,均为1,形状为(3,4)满足均值为0方差为1的张量
torch.tensor(l:list) # 输入数组创建张量
```
## 2.数据运算
``` python
# + - * / ** 都是对每个元素进行运算
torch.exp(x) # 对x的每个元素进行e^x
torch.cat(X,Y,dim=0) # 张量连接,dim为0是行连接,dim为1是列连接
X == Y # 返回对应元素相等对应位置上的元素为True的张量
x.sum() # 求和 可以指定axis=0是所有列分别相加形成的一行(沿着列),为1是所有行分别相加形成的一行,这个时候通过指定keepdims=True保持轴数不变还是一列
A.cumsum(axis=0) # 不会降低维度,相当于把x.sum()追加到了最后一行
```
**广播机制**:形状不同的张量进行操作的时候会进行自动复制扩充以匹配运算。
**索引和切片**:张量特性和Python数组特性类似有索引和切片
**内存节省**: `X = X + Y`与`X += Y`的区别,在Python中如果`X`是简单数据类型(如整数、浮点数、字符等)那么这两者没有区别,如果是复杂数据类型前者可能会重新创建一个对象并赋给`X`后者将在原有内存上进行赋值,所以后者是更加节省内存的(或者写作`X[:] = X + Y`)。
**对象转换**:将torch的张量转换为numpy的张量(ndarray),这两其实看上去都是Python的数组,转换也就理所当然了。转换后是共享底层内存的,所以对变量的操作将会相互影响。
```python
A = X.numpy()
B = torch.tensor(A)
B.item() # 只有一个元素的张量(长度为1)可以直接转换为Python数据类型,也可以直接int(B),float(B)
```
## 3.数据预处理
数据预处理是将原始数据处理成张量的过程。
```python
# 通过Python的pandas库读取csv文件的数据
import pandas as pd
data = pd.read_csv(data_file)
data.iloc # 位置索引相当于切片
data = {
'NumRooms': [None, 2.0, 4.0, None],
'Alley': ['Pave', None, None, None],
'Price': [127500, 106000, 178100, 140000]
}
data = pd.DataFrame(data)
inputs, outputs = data.iloc[:, 0:2], data.iloc[:, 2]
inputs = inputs.fillna(inputs.mean(numeric_only=True)) # 以平均值填充NaN(缺失的数据)
inputs = pd.get_dummies(inputs, dummy_na=True)
# pd.get_dummies() 函数将分类变量的每个类别转换为一个二进制列(0 或 1),其中 1 表示观察值属于该类别,0 表示不属于.
# dummy_na参数表示将NaN值单独作为一个类别
x = torch.tensor(inputs.to_numpy(dtype=float)) # 将pandas数据转换为pytorch的张量
```
## 4.数学基础
### 4.1 线代
```python
A.T # 矩阵A的转置
torch.dot(x,y) # x,y两个向量的点积
torch.mv(A,x) # 矩阵-向量积(matrix-vector product)
torch.mm(A,B) # 矩阵-矩阵乘法(matrix-matrix multiplication)
torch.norm(u) # L2范数,也就是求向量元素平方和的平方根
torch.abs(u).sum # 另一种L1范数,即绝对值求和
# L1范数和L2范数都是更一般的范数Lp(Frobenius范数)的特例,也是使用torch.norm方法
```
### 4.2 微积分
```python
%matplotlib inline
# 这是jupyter魔术命令将图像直接显示嵌入
import numpy as np
from matplotlib_inline import backend_inline
from matplotlib import pyplot as plt
def f(x):
return 3 * x ** 2 - 4 * x
# 以下就是一些绘图的代码,后续需要绘图可以重用,这部分代码已经包含在本书配套软件包d2l中
def use_svg_display(): #@save
"""使用svg格式在Jupyter中显示绘图"""
backend_inline.set_matplotlib_formats('svg')
def set_figsize(figsize=(3.5, 2.5)): #@save
"""设置matplotlib的图表大小"""
use_svg_display()
plt.rcParams['figure.figsize'] = figsize
#@save
def set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend):
"""设置matplotlib的轴"""
axes.set_xlabel(xlabel)
axes.set_ylabel(ylabel)
axes.set_xscale(xscale)
axes.set_yscale(yscale)
axes.set_xlim(xlim)
axes.set_ylim(ylim)
if legend:
axes.legend(legend)
axes.grid()
#@save
def set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend):
"""设置matplotlib的轴"""
axes.set_xlabel(xlabel)
axes.set_ylabel(ylabel)
axes.set_xscale(xscale)
axes.set_yscale(yscale)
axes.set_xlim(xlim)
axes.set_ylim(ylim)
if legend:
axes.legend(legend)
axes.grid()
#@save
def plot(X, Y=None, xlabel=None, ylabel=None, legend=None, xlim=None,
ylim=None, xscale='linear', yscale='linear',
fmts=('-', 'm--', 'g-.', 'r:'), figsize=(3.5, 2.5), axes=None):
"""绘制数据点"""
if legend is None:
legend = []
set_figsize(figsize)
axes = axes if axes else plt.gca()
# 如果X有一个轴,输出True
def has_one_axis(X):
return (hasattr(X, "ndim") and X.ndim == 1 or isinstance(X, list)
and not hasattr(X[0], "__len__"))
if has_one_axis(X):
X = [X]
if Y is None:
X, Y = [[]] * len(X), X
elif has_one_axis(Y):
Y = [Y]
if len(X) != len(Y):
X = X * len(Y)
axes.cla()
for x, y, fmt in zip(X, Y, fmts):
if len(x):
axes.plot(x, y, fmt)
else:
axes.plot(y, fmt)
set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
x = np.arange(0, 3, 0.1)
plot(x, [f(x), 2 * x - 3], 'x', 'f(x)', legend=['f(x)', 'Tangent line (x=1)'])
```
非代码部分的偏导数以及梯度等等概念高数书中有详细介绍不在此赘述。
### 4.3 自动微分
框架提供自动微分来加快求导,`自动微分使系统能够随后反向传播梯度。这里,反向传播(backpropagate)意味着跟踪整个计算图,填充关于每个参数的偏导数。`书中这句话说明了自动求导的意义,现在还不是很懂,后续遇到应该会有所了解。
- `x.requires_grad_(True) # 等价于x=torch.arange(4.0,requires_grad=True)`
这意味着在计算图中,`x` 将被视为一个需要计算梯度的节点。当你对这个张量执行操作并最终计算损失函数时,PyTorch 会自动跟踪所有对 `x` 的操作,并在反向传播时计算关于 `x` 的梯度。
```python
x.grad # 默认会是None
y = 2 * torch.dot(x, x) # 点积
y.backward()
x.grad # 通过调用反向传播函数来自动计算y关于x每个分量的梯度
x.grad.zero_() # 在默认情况下,PyTorch会累积梯度,我们需要清除之前的值
```
==感觉这里需要补一下[矩阵微分(向量微分)](https://gaomj.cn/matrixgradient/)、[矩阵梯度](https://zlearning.netlify.app/math/matrix/matrix-gradient.html)以及反向传播的知识。(后续再补吧,头大)==
以上的微分简而言之是下面这个公式:
$\nabla_{\mathbf{x}} f(\mathbf{x})=\left[\frac{\partial f(\mathbf{x})}{\partial x_1}, \frac{\partial f(\mathbf{x})}{\partial x_2}, \ldots, \frac{\partial f(\mathbf{x})}{\partial x_n}\right]^{\top}$
```python
# 对非标量调用backward需要传入一个gradient参数,该参数指定微分函数关于self的梯度。
# 本例只想求偏导数的和,所以传递一个1的梯度是合适的
x.grad.zero_()
y = x * x
# 等价于y.backward(torch.ones(len(x)))
y.sum().backward()
x.grad
```
==到这里只是似懂非懂了...:cry:==
### 4.4 概率
```python
%matplotlib inline
import torch
from torch.distributions import multinomial
# 模拟掷骰子的过程
fair_probs = torch.ones([6]) / 6 # 这个向量表示了一个公平的六面骰子每个面出现的概率。
multinomial.Multinomial(1, fair_probs).sample() # 创建一个多项式分布对象,其中1表示我们只抽取一个样本,fair_probs是我们刚才创建的公平概率向量。.sample()方法用于从这个分布中抽取一个样本。
multinomial.Multinomial(10, fair_probs).sample()
# tensor([0., 2., 1., 2., 4., 1.]) 可能结果10次独立的抽样,分别得到了上述的索引值,每个索引值对应于一个特定的结果。
```
# 第三讲 线性神经网络
主要包含两个部分:线性回归和softmax回归。
## 3.1 线性回归
### 3.1.1 线性模型
即线性拟合,这与高中所学过的概率知识契合,不做过多叙述(其实这里用矩阵描述的)。
### 3.1.2 损失函数
$$
L(\mathbf{w}, b)=\frac{1}{n} \sum_{i=1}^n l^{(i)}(\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 .
$$
损失函数就是描述估计值和观测值之间的误差的函数。在训练模型时,我们希望寻找一组参数$\left(\mathbf{w}^*, b^*\right)$, 这组参数能最小化在所有训练样本上的总损失。
$$
\mathbf{w}^*, b^*=\underset{\mathbf{w}, b}{\operatorname{argmin}} L(\mathbf{w}, b) .
$$
### 3.1.3 解析解
$\|\mathbf{y}-\mathbf{X} \mathbf{w}\|^2$最小化(也就是让损失关于$\mathbf{w}$的导数设为0)得到解析解:
==这里是怎么推算的也不是很清楚:anguished:后续补上==
$$
\mathbf{w}^*=\left(\mathbf{X}^{\top} \mathbf{X}\right)^{-1} \mathbf{X}^{\top} \mathbf{y}
$$
线性回归存在解析解,但不是所有问题存在解析解,所以无法广泛应用在DL。
### 3.1.4 随机梯度下降
无法得到解析解的情况下,可以通过**梯度下降**的方法通过不断地在损失函数递减的方向上更新参数来降低误差。对于平方损失和仿射变换就是以下形式:
$$
\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}
$$
$|\mathcal{B}|$表示每个小批量中的样本数,$\eta$表示学习率,批量大小和学习率的值通常是手动预先指定(称为超参数),而不是通过模型训练得到的。 训练集上的损失达到最小并没有太大作用,难的是找到一组参数,这组参数能够在我们从未见过的数据上实现较低的损失(也称为泛化)。
### 3.1.5 矢量化加速
使用循环遍历向量的效率是不如向量直接相加的,大概是因为这些库对运算已经做好了优化吧。
### 3.1.6 正态分布与平方损失
$$
p(x)=\frac1{\sqrt{2\pi\sigma^2}}\mathrm{exp}\left(-\frac1{2\sigma^2}(x-\mu)^2\right).
$$
```python
# 正态函数
def normal(x, mu, sigma):
p = 1 / math.sqrt(2 * math.pi * sigma**2)
return p * np.exp(-0.5 / sigma**2 * (x - mu)**2)
# 绘制三个不同的正态分布
params = [(0, 1), (0, 2), (3, 1)]
d2l.plot(x,[normal(x,mu,sigma) for mu,sigma in params],xlabel='x',ylabel='p(x)',figsize=(4.5,2.5),legend=[f'mean {mu}, std {sigma}' for mu, sigma in params]) # figsize控制图形的显示大小,legend指定图例
```
最小化负对数似然:
$$
-\log P(\mathbf{y}\mid\mathbf{X})=\sum_{i=1}^n\frac12\mathrm{log}(2\pi\sigma^2)+\frac1{2\sigma^2}\left(y^{(i)}-\mathbf{w}^\top\mathbf{x}^{(i)}-b\right)^2.
$$
## 3.2 线性回归的从零开始实现
### 3.2.1 生成数据集
```python
def synthetic_data(w, b, num_examples): #@save
"""生成y=Xw+b+噪声"""
X = torch.normal(0, 1, (num_examples, len(w)))
y = torch.matmul(X, w) + b
# torch.matmul函数的行为取决于输入张量的形状:
# 如果两个输入都是1维张量(向量),它将执行点积(dot product)。
# 如果至少一个输入是2维张量(矩阵),它将执行矩阵乘法。
# 如果输入的维度超过2维,它将执行批量矩阵乘法。
y += torch.normal(0, 0.01, y.shape) # 将标准差设为0.01
return X, y.reshape((-1, 1))
true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)
d2l.set_figsize()
d2l.plt.scatter(features[:, (1)].detach().numpy(), labels.detach().numpy(), 1)
# detach()方法用于创建一个新的张量,这个新张量与原始张量共享数据,但不包含梯度信息
# 需要注意的是,.numpy()方法只能用于CPU张量。如果张量在GPU上(即它的设备是CUDA),你需要先将其移动到CPU上,然后才能转换为NumPy数组。
```
> 后续越来越不知道是在干啥了,感觉还是需要配合《深度学习》(花书)把概念先过一遍才好理解,那就先改变学习计划吧,算是在计划之外了,高估了自己的基础。还有我使用的是Typora编辑Markdown文档,论坛的Markdown编辑器好像不支持LaTex数学公式渲染,那读者只能自行复制到自己的Markdown编辑器查看了,下篇笔记看能不能解决。
- 2024-08-22
-
回复了主题帖:
读书入围名单: 《动手学深度学习(PyTorch版)》,配套视频、源码等
个人信息无误,确认可以完成阅读分享计划