视频:https://space.bilibili.com/1567748478/channel/seriesdetail?sid=358497
课程主页:https://courses.d2l.ai/zh-v2/
教材:https://zh-v2.d2l.ai/
课程论坛讨论:https://discuss.d2l.ai/c/16
Github:https://github.com/d2l-ai/d2l-zh
Pytorch论坛:https://discuss.pytorch.org/
使用conda环境
conda env remove d2l-zh # 移除原有的环境
conda create -n d2l-zh python=3.8 # 创建d2l-zh环境,-n表示后面的参数是名字
conda activate d2l-zh # 切换到d2l-zh环境
安装需要的包
pip install jupyter d2l torch torchvision
下载代码并执行
wget https://zh-v2.d2l.ai/d2l-zh.zip
unzip d2l-zh.zip
jupyter notebook
id(Y)
Y = Y + X
左边的Y的id和右边的Y的id不一样,可以认为右边Y对应的内存已经被析构了。
执行原地操作可以避免。
Z = torch.zeros_like(Y)
Z[:] = X + Y # 这样Z的id不会变
# 如果后续不用X,则可以用下面两种来减少操作的内存开销
X[:] = X + Y
# 或
X += Y # X的id不会发生变化
- 创建数据
import os
os.makedirs()
os.path.join('..', 'data', 'house_tiny.csv')
with open(data_file, 'w') as f:
f.write('...\n')
- 加载数据
import pandas as pd
pd.read_csv(data_file)
- 拆分成输入输出
input, output
- 处理缺失值(均值)
inputs.fillna(inputs.mean())
- one-hot编码
pd.get_dummies(inputs, dummy_na=True)
- 转换为tensor
import torch
torch.tensor(inputs.values)
转torch,一般默认数据类型式torch,float64,但是通常用float32浮点数,因为后者计算更快。
a = torch.arange(12)
b = a.reshape((3,4))
b[:] = 2
a # a也会改掉,tensor([2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2])
在python中:
赋值是把原对象的引用给到新对象
浅拷贝是重新分配一块内存,创建一个新的对象,但里面的元素是原对象中各个子对象的引用
深拷贝是重新分配一块内存,创建一个新的对象,并且将原对象中的元素,以递归的方式,通过创建新的子对象拷贝到新对象中。因此,新对象和原对象没有任何关联。
参考:https://blog.csdn.net/qq_40630902/article/details/119278072
X = torch.arange(24).reshape(2,3,4)
应该这样理解,这是一个三维张量,它由2个二维矩阵组成,每个二维矩阵包含3个行向量
每个行向量包含4个标量
torch中的深拷贝是torch.clone()方法
sum_A = A.sum(axis=1, keepdims=True)
不设置keepdims,求和会把向量变成标量,维度会被丢掉
如5*4的张量,对第1维求和,则会变成5*1的张量,然后第1维被丢掉,最终变成长为5的张量
但如果设置keepdims,则第1维不会被丢到,最终是5*1的张量
这样做的好处是,可以用广播机制实现`A / sum_A`
这里A的形状是(5,4),sum_A的形状是(5,1),符合广播规则。
除了使用keepdims参数,也可以使用tensor对象的unsqueeze方法来扩充维度。
广播规则:
如果对于每个’后缘维度’(trailing dimension)(即从末端开始),轴长度匹配(轴长度相等),或者如果其中一个长度为1,则两个数组兼容广播。然后在缺失或长度为1的维度上执行广播。
x = torch.arange(4.0, requires_grad=True)
y = 2 * torch.dot(x, x)
y.backward()
x.grad
x.grad_zero()
y = x * x
u = y.detach() # 把结果固定住,而不再是x的函数
z = u * x
z.sum().backward()
x.grad == u # TRUE
这个在需要固定网络中的参数时很有用。
默认情况下,Pytorch会累计梯度,所以需要用x.grad_zero()清除之前的值
推荐阅读:
Pytorch autograd,backward详解
pytorch 中retain_graph==True的作用
features, labels = synthetic_data(true_w, true_b, 1000)
def data_iter(batch_size, features, labels): # 生成器函数
num_examples = len(features)
indices = list(range(num_examples)
random.shuffle(indices) # 将样本打乱,使批次里面的样本数据并不是连续的
...
yield features[batch_indices], labels[batch_indices]
测试代码
w = torch.normal(0, 0.01, size=(2,1), requires_grad=True) # 均值为0,标准差为1的数组成的2行1列的张量
b = torch.zeros(1, requires_grad=True)
def linreg(X, w, b):
return torch.matmul(X, w) + b # X是n*2的张量,w是2*1的张量,b是标量
torch.mul(a,b):哈达玛积 等价于
a*b
torch.mm(a,b):正常矩阵运算,只适用于二维矩阵
torch.matmul(a,b):正常矩阵运算,不仅适用于二维,还适用于高维矩阵
def squared_loss(y_hat, y):
return (y_hat - y.reshape(y_hat.shape))**2 / 2
# 注意这里没有求和,也没有求均值,所以并不是严格的损失函数
def sgd(params, lr, batch_size):
with torch.no_grad():
for param in params:
# 因为计算的loss是一个批量样本的总和,所以求均值时的分母是batch_size
param -= lr * param.grad / batch_size
param.grad.zero_()
requires_grad=True的张量无法原地更新。但加上with torch.no_grad()就可以了。这样就实现了参数更新。
为了不让梯度累加,注意一定要在每次更新权重后进行梯度清零。
推荐阅读:
几行代码让你搞懂torch.no_grad
# 指定超参数
lr = 0.03
num_epochs = 3 # 整个数据扫三遍
net = linreg # 用net变量记录模型,方便后续更改模型时修改
loss = squared_loss
for epoch in range(num_epochs):
for X, y in data_iter(batch_size, features, labels): # 拿出一个批量大小的X和y
l = loss(net(X, w, b), y) # 预测的y和真实的y求小批量损失,得到批量大小*1的张量
l.sum().backward() # 求和,反向传播
sgd([w, b], lr, batch_size) # 梯度下降法更新参数
# 事实上,对于最后一个批量的样本,可能达不到batch_size的数量,所以这个batch_size更换成len(y)更好
with torch.no_grad():
trian_l = loss(net(features, w, b), labels) # 每完成一个epoch的训练,在整个训练集上计算损失
print(f"epoch {epoch + 1}, loss {float(train_l.mean()):f}") # :f表示:.6f保留小数点后6位
-------------
epoch 1, loss 0.040831
epoch 2, loss 0.000154
epoch 3, loss 0.000052
from torch.utils import data # 有一些处理数据的模块
def load_array(data_arrays, batch_size, is_train=True):
"""构造一个PyTorch数据迭代器"""
dataset = data.TensorDataset(*data_arrays)
return data.DataLoader(dataset, batch_size, shuffle=is_train)
batch_size = 10
data_iter = load_array((features, labels), batch_size)
next(iter(data_iter)) # 需要通过iter转化成迭代器
from torch import nn
net = nn.Squential(nn.Linear(2, 1))
nn.Squential()是一个有序的容器,神经网络模块将按照在传入构造器的顺序依次被添加到计算图中执行,神经网络模块为元素的有序字典也可以作为传入参数
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)
loss = nn.MSELoss()
trainer = torch.optim.SGD(net.parameters(), lr=0.03)
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操作子得到每个类的预测置信度
使用交叉熵来衡量预测和标号的区别
import torchvision # pytorch对计算机视觉模型实现的一些库
from torchvision import transforms # 对数据进行操作的模块
d2l.use_svg_display() # 用svg来显示图片,这样清晰度高一点
# 通过Totensor实例将图像数据从PIL类型变换成32位浮点数格式
# 并除以255使得所有像素的数值均在0到1之间
trans = transforms.ToTensor()
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)
len(mnist_train), len(mnist_test) # (60000, 10000)
# 第一个0表示第0个example, 第二个0表示图片标号
mnist_train[0][0].shape # torch.Size([1, 28, 28])
def get_fashion_mnist_labels(labels):
"""返回Fashion-MNIST数据集的文本标签。"""
def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5)
"""绘制图像列表"""
def get_dataloader_workers()
"""指定读取数据的进程数,这里指定为4"""
return 4
train_iter = data.DataLoader(mnist_train, batch_size, shuffle=True,
num_workers=get_dataloader_workers())
# 测试读取数据的耗时,根据CPU性能而不同
timer = d2l.Timer()
for X, y in train_iter:
continue
f"{timer.stop():.2f} sec"
很可能模型训练挺快的,但数据读不过来。所以通常会在训练之前,看一下数据读取有多快。至少要保证读的比训练要快。
def load_data_fashion_mnist(batch_size, resize=None):
"""下载Fashion-MNIST数据集,然后将其加载到内存中。"""
"""resize参数用于后续调整图片大小"""
trian_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
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)
softxmax回归要求图像展平成向量。
展平会损失空间信息,这留到卷积神经网络探讨。
def softmax(X):
X_exp = torch.exp(X)
partition = X_exp.sum(1, keepdim=True)
return X_exp / partition # 应用广播机制
这里可以在开头加一行X -= X.max(),避免上界溢出
def net(X):
return softmax(torch.matmul(X.reshape(-1, W.shape[0]), W) + b)
先举个例子
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] # fancy index
def cross_entropy(y_hat, y):
return -torch.log(y_hat[range(len(y_hat)), y])
def accuracy(y_hat, y):
"""计算预测正确的数量"""
def evaluate_accuracy(net, data_iter):
"""计算在指定数据集上模型的精度"""
class Accumulator:
def train_epoch_ch3(net, train_iter, loss, updater):
"""训练一个epoch,返回训练的平均损失和精度"""
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater):
lr = 0.1
def updater(batch_size):
return d2l.sgd([W,b], lr, batch_size)
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)
net.apply(init_weights); # 会对每个层执行一次init_weights函数
loss = nn.CrossEntropyLoss()
trainer = torch.optim.SGD(net.parameters(), lr=0.1)
num_epochs = 10
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
问题1:softlabel训练策略
答:由于很难用softmax逼近0和1,所以考虑将正确的类的真实概率记为0.9,不正确的记录为0.1/(n-1)
。这是图片分类里大家默认会使用的trick。
问题21:在计算精度时,为什么需要使用net.eval()将模型设置成评估模式?
答:在pytorch官方文档中提到,eval和train的区别是BN、dropout的处理不同。
Sigmoid激活函数
将输入投影到(0,1)
s i g m o i d ( x ) = 1 1 + e x p ( − x ) sigmoid(x) = \frac{1}{1+exp(-x)} sigmoid(x)=1+exp(−x)1
Tanh激活函数
将输入投影到(-1,1)
t a n h ( x ) = 1 − e x p ( − 2 x ) 1 + e x p ( − 2 x ) tanh(x) = \frac{1-exp(-2x)}{1+exp(-2x)} tanh(x)=1+exp(−2x)1−exp(−2x)
ReLU激活函数
rectified linear unit
R e L U ( x ) = m a x ( x , 0 ) ReLU(x) = max(x, 0) ReLU(x)=max(x,0)
总结
多层感知机使用隐藏层和激活函数来得到非线性模型
常用激活函数是Sigmoild,Tanh,ReLU
使用Softmax来处理多类分类
超参数为隐藏层数,和各个隐藏层大小
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)
net.apply(init_weights); # 加入分号则不会输出内容
batch_size, lr, num_epochs = 256, 0.1, 10
loss = nn.CrossEntropyLoss()
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)
训练数据集:训练模型参数
验证数据集:选择模型超参数
测试数据集:只用一次,用于
非大数据集上通常使用k-折交叉验证
在没有足够多数据时使用
算法:
有时候为了把泛化误差往下降,不得不承受一定程度的过拟合
模型容量得够大,然后再去控制它的容量。这是整个深度学习的核心。
问题1:SVM从理论上讲应该对分类效果不错,和神经网络相比,缺点在哪里?
答:SVM通过kernel匹配模型复杂度,算起来不容易。大数据用SVM速度慢。
当数据不大,十万个点以内,SVM都可以做,比较容易解。
SVM能调整的不多。
神经网络可以讲特征提取和分类做在一起。
问题17:如何有效设计超参数?最好的搜索是贝叶斯方法还是网格、随机?
答:这是一个很大的领域的问题,叫HPO。
超参数的设计靠专家经验。
搜索:试一个然后调整,或者直接随机。
问题18:假设做二分类问题,实际情况是1/9的比例,我的训练集两种类型的比例应该是1/1还是1/9?
答:验证集最好是1/1。
问题19:k折交叉验证的目的是确定超参数吗?然后还要用这个超参数再训练一遍全部数据吗?
答:
最常见的做法:k折交叉验证确定超参数,然后再训练一遍全部数据。
第二个做法:不再重新训练了,找出精度最好的那一折的模型。
第三个做法:k个模型都保留,每次预测k个模型都跑,然后取均值(集成)。
通过限制模型参数的范围来限制模型容量
这样优化起来困难,一般不用。常用的是下面的方法
使用均方范数作为柔性限制
对每个 θ \theta θ,都可以找到 λ \lambda λ使得之前的目标函数等价于下面的目标函数
m i n l ( w , b ) + λ 2 ∣ ∣ w ∣ ∣ 2 min\ \ l(w,b)+\frac{\lambda}{2}||w||^2 min l(w,b)+2λ∣∣w∣∣2
可以通过拉格朗日乘子来证明。
超参数 λ \lambda λ控制了正则项的重要程度
总结
权重衰退通过L2正则项使得模型参数不会过大,从而控制模型复杂度。
正则项权重是控制模型复杂度的超参数
可以将L2正则项直接写在目标函数内,也可以直接等价将权重衰退写到优化器里面。
trainer = torch.optim.SGD([{
"params": net[0].weight,
"weight_decay": wd}, {
"params": net[0].bias}], lr=lr)
推理中的丢弃法
dropout是一个正则项,正则项只在训练中使用。
推理过程中,丢弃法直接返回输入。这样保证输出是确定性的。
总结
丢弃法将一些输出项随机置0来控制模型复杂度
常作用在多层感知机的隐藏层输出上(很少用在CNN等上面)
丢弃概率是控制模型复杂度的超参数(常取0.1,0.5,0.9)
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)
# 在GPU下,做乘法比通过X[mask]=0快
dropout1, dropout2 = 0.2, 0.5
class Net(nn.Module):
def __init__(self, num_inputs, num_outputs, num_hiddens1, num_hiddens2,
is_training = True):
super(Net, self).__init__()
self.num_inputs = num_inputs
self.training = is_training
self.lin1 = nn.Linear(num_inputs, num_hiddens1)
self.lin2 = nn.Linear(num_hiddens1, num_hiddens2)
self.lin3 = nn.Linear(num_hiddens2, num_outputs)
self.relu = nn.ReLU()
def forward(self, X):
H1 = self.relu(self.lin1(X.reshape((-1, self.num_inputs))))
# 只有在训练模型时才使用dropout
if self.training == True:
# 在第一个全连接层之后添加一个dropout层
H1 = dropout_layer(H1, dropout1)
H2 = self.relu(self.lin2(H1))
if self.training == True:
# 在第二个全连接层之后添加一个dropout层
H2 = dropout_layer(H2, dropout2)
out = self.lin3(H2)
return out
net = Net(num_inputs, num_outputs, num_hiddens1, num_hiddens2)
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);
问题11:dropout随机置0对求梯度和反向传播的影响是什么?
答:梯度会变0,dropout的权重这一轮不会被更新。
神经网络很深的时候,非常容易数值不稳定。
会产生两个问题:梯度爆炸和梯度消失。
让训练更加稳定
下载数据
使用pandas读入并处理数据(去掉ID和label得到features)
将数值特征重新缩放到“均值为0,方差为1”来标准化数值特征。
将数值特征的缺失值填充为0。
处理离散值-one hot encoding。
all_features = pd.get_dummies(all_features, dummy_na=True
转换为张量
关心相对误差,解决这个问题的一个方法是用价格预测的对数来衡量差异
def log_rmse(net, features, labels):
# 为了在取对数时进一步稳定该值,将小于1的值设置为1
clipped_preds = torch.clamp(net(features), 1, float('inf')) # 把数据限制在1-无穷之间
rmse = torch.sqrt(loss(torch.log(clipped_preds),
torch.log(labels)))
return rmse.item()
借助Adam优化器训练模型(Adam对学习率没SGD敏感)
调参
推理-提交预测