文章目录
资料
视频: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
4.数据操作+数据预处理
4.1 数据操作实现
- 运行一些操作可能导致为新结果分配内存
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不会发生变化
4.2 数据预处理
- 创建数据
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浮点数,因为后者计算更快。
4.3数据操作QA
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
5. 线性代数
5.2 线性代数实现
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的维度上执行广播。
7. 自动求导
- 自动求导
x = torch.arange(4.0, requires_grad=True)
y = 2 * torch.dot(x, x)
y.backward()
x.grad
- 分离计算
通过detach()方法将某些计算移动到记录的计算图之外
x.grad_zero()
y = x * x
u = y.detach() # 把结果固定住,而不再是x的函数
z = u * x
z.sum().backward()
x.grad == u # TRUE
这个在需要固定网络中的参数时很有用。
默认情况下,Pytorch会累计梯度,所以需要用x.grad_zero()清除之前的值
- Python控制流的梯度计算
即使构建函数的计算图需要通过Python控制流(例如,条件、循环或任意函数调用),仍然可以计算得到的变量的梯度
推荐阅读:
Pytorch autograd,backward详解
pytorch 中retain_graph==True的作用
8. 线性回归
8.2 基础优化算法
- 学习率
学习率是步长的超参数。 - 小批量随机梯度下降
在整个训练集上算梯度太贵。所以我们可以随机采样b个样本来近似损失。b是批量大小,是另一个重要的超参数。
8.3 线性回归的从零开始实现
- 构造训练数据集
features, labels = synthetic_data(true_w, true_b, 1000)
- 生成批次数据
定义data_iter函数,接受批量大小、特征矩阵和标签向量作输入,生成大小为batch_size的小批量
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
8.4 线性回归的简洁实现
from torch.utils import data # 有一些处理数据的模块
- 调用框架中现有的API来读取数据
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()
- 实例化SGD实例
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}")
8.5 线性回归QA
- 问题8:batchsize是否会最终影响模型结果?batchsize过小是否可能导致最终累计的梯度计算不准确?
答:batchsize越小反而收敛效果越好。随机梯度下降实际上是带来噪音,采样样本越小,噪音越多。但噪音对神经网络是好事情。因为现在神经网络太复杂,一定的噪音可以防止过拟合。 - 问题17:这样的data-iter写法,每次都把所有输入load进入,如果数据多的话,最后内存会爆掉把?有什么好的办法呢?
答:确实有可能。实际数据在硬盘中,每次只需要读到批量大小的数据进内存就行了。 - 问题21:如果样本大小不是批量的整数倍,那需要随机剔除多余的样本吗?
答:三种做法:1.最后一个批量不足批量大小。2.丢掉最后一个批量。3.补全这个批量。 - 问题24:老师请问这里面是没有收敛的判断吗?直接人为设置epoch大小吗?
答:可以判收敛。如两个epoch之间,目标函数变化不大。或者拿一个验证数据集,在验证数据集上精度没有增加了停。
9. Softmax回归
9.1 Softmax回归
Softmax回归是一个多类分类模型
使用Softmax操作子得到每个类的预测置信度
使用交叉熵来衡量预测和标号的区别
9.2 图片分类数据集
import torchvision # pytorch对计算机视觉模型实现的一些库
from torchvision import transforms # 对数据进行操作的模块
d2l.use_svg_display() # 用svg来显示图片,这样清晰度高一点
- 通过框架中的内置函数将Fashion-MNIST数据集下载并读取到内存中
# 通过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)
"""绘制图像列表"""
- 读取一小批数据,大小为batch_size
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"
很可能模型训练挺快的,但数据读不过来。所以通常会在训练之前,看一下数据读取有多快。至少要保证读的比训练要快。
- 定义load_data_fashion_mnist函数
def load_data_fashion_mnist(batch_size, resize=None):
"""下载Fashion-MNIST数据集,然后将其加载到内存中。"""
"""resize参数用于后续调整图片大小"""
9.3 Softmax回归从零开始实现
- 读取数据
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回归要求图像展平成向量。
展平会损失空间信息,这留到卷积神经网络探讨。
- 实现softmax
def softmax(X):
X_exp = torch.exp(X)
partition = X_exp.sum(1, keepdim=True)
return X_exp / partition # 应用广播机制
这里可以在开头加一行X -= X.max(),避免上界溢出
- 实现softmax回归模型
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
这种提取张量的子集的方式叫做fancy index(花式索引)
下面描述了pandas的花式索引
- 定义交叉熵损失函数
def cross_entropy(y_hat, y):
return -torch.log(y_hat[range(len(y_hat)), y])
- 定义准确率函数
def accuracy(y_hat, y):
"""计算预测正确的数量"""
- 评估任意模型net的准确率
def evaluate_accuracy(net, data_iter):
"""计算在指定数据集上模型的精度"""
- 定义累加器类
class Accumulator:
- Sofxmax回归的训练
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)
9.4 Softmax回归简洁实现
- 定义模型和初始化参数
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)
9.5 QA
问题1:softlabel训练策略
答:由于很难用softmax逼近0和1,所以考虑将正确的类的真实概率记为0.9,不正确的记录为0.1/(n-1)
。这是图片分类里大家默认会使用的trick。
问题21:在计算精度时,为什么需要使用net.eval()将模型设置成评估模式?
答:在pytorch官方文档中提到,eval和train的区别是BN、dropout的处理不同。
10.多层感知机
10.2 多层感知机
-
单隐藏层-单分类
不加非线性激活函数的多层感知机,等价于单层感知机。 -
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来处理多类分类
超参数为隐藏层数,和各个隐藏层大小
10.3 多层感知机的简洁实现
- 构建网络,初始化参数
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)
11. 模型选择
11.1 模型选择
训练数据集:训练模型参数
验证数据集:选择模型超参数
测试数据集:只用一次,用于
非大数据集上通常使用k-折交叉验证
11.1.1 K折交叉验证
在没有足够多数据时使用
算法:
- 将训练数据分割成K块
- For i = 1, …, k
- 使用第i块作为验证数据集,其余作为训练数据集
- 报告k个验证集误差的平均
常用:k=5或者10
11.2 过拟合和欠拟合
有时候为了把泛化误差往下降,不得不承受一定程度的过拟合
模型容量得够大,然后再去控制它的容量。这是整个深度学习的核心。
11.2.1 估计模型容量
- 难以在不同种类算法之间比较
- 如树模型和神经网络
- 给定一个模型种类,将有两个主要因素
- 可学习的参数的个数
- 参数值的选择范围
11.2.2 数据复杂度
- 多个重要因素
- 样本个数
- 每个样本的元素个数
- 时间、空间结构
- 多样性
11.4 QA
问题1:SVM从理论上讲应该对分类效果不错,和神经网络相比,缺点在哪里?
答:SVM通过kernel匹配模型复杂度,算起来不容易。大数据用SVM速度慢。
当数据不大,十万个点以内,SVM都可以做,比较容易解。
SVM能调整的不多。
神经网络可以讲特征提取和分类做在一起。
问题17:如何有效设计超参数?最好的搜索是贝叶斯方法还是网格、随机?
答:这是一个很大的领域的问题,叫HPO。
超参数的设计靠专家经验。
搜索:试一个然后调整,或者直接随机。
问题18:假设做二分类问题,实际情况是1/9的比例,我的训练集两种类型的比例应该是1/1还是1/9?
答:验证集最好是1/1。
问题19:k折交叉验证的目的是确定超参数吗?然后还要用这个超参数再训练一遍全部数据吗?
答:
最常见的做法:k折交叉验证确定超参数,然后再训练一遍全部数据。
第二个做法:不再重新训练了,找出精度最好的那一折的模型。
第三个做法:k个模型都保留,每次预测k个模型都跑,然后取均值(集成)。
12. 权重衰退
通过限制模型参数的范围来限制模型容量
12.1 权重衰退
- 使用均方范数作为硬性限制
通过限制参数值的选择范围来控制模型容量
m i n l ( w , b ) s u b j e c t t o ∣ ∣ w ∣ ∣ 2 ≤ θ min l(w,b)\ subject\ to ||w||^2\leq \theta minl(w,b) subject to∣∣w∣∣2≤θ
这样优化起来困难,一般不用。常用的是下面的方法
-
使用均方范数作为柔性限制
对每个 θ \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 λ控制了正则项的重要程度- λ = 0 \lambda=0 λ=0:无作用
- λ → ∞ , w ∗ → 0 \lambda\rightarrow \infty, w^*\rightarrow0 λ→∞,w∗→0
-
参数更新法则
-
总结
权重衰退通过L2正则项使得模型参数不会过大,从而控制模型复杂度。
正则项权重是控制模型复杂度的超参数
12.2 简洁实现
可以将L2正则项直接写在目标函数内,也可以直接等价将权重衰退写到优化器里面。
trainer = torch.optim.SGD([{
"params": net[0].weight,
"weight_decay": wd}, {
"params": net[0].bias}], lr=lr)
13.丢弃法
- 动机
一个好的模型需要对输入数据的扰动鲁棒
使用有噪音的数据等价于Tikhonov正则
丢弃法:在层之间加入噪音
13.1 丢弃法
-
原理
-
训练中使用丢弃法
-
推理中的丢弃法
dropout是一个正则项,正则项只在训练中使用。
推理过程中,丢弃法直接返回输入。这样保证输出是确定性的。 -
总结
丢弃法将一些输出项随机置0来控制模型复杂度
常作用在多层感知机的隐藏层输出上(很少用在CNN等上面)
丢弃概率是控制模型复杂度的超参数(常取0.1,0.5,0.9)
13.2 代码实现
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);
13.3 QA
问题11:dropout随机置0对求梯度和反向传播的影响是什么?
答:梯度会变0,dropout的权重这一轮不会被更新。
14 数值稳定性+模型初始化和激活函数
14.1 数值稳定性
神经网络很深的时候,非常容易数值不稳定。
会产生两个问题:梯度爆炸和梯度消失。
- 梯度爆炸的问题
- 值超出值域:对16位浮点数尤其严重
- 对学习率敏感
学习率太大->大参数值->更大的梯度
学习率太小->训练没有进展
可能需要在训练过程中不断调整学习率
- 梯度消失的问题
- 梯度值变为0:一个很小的数会被认为是0,对16位浮点数尤其严重
- 训练没有进展:无论怎么选学习率
- 对底部层尤其严重:仅顶部层训练的较好;无法让神经网络更深
14.2 模型初始化和激活函数
让训练更加稳定
- 目标:让梯度值在合理的范围内,如[1e-6,1e3]
- 将乘法变成加法:ResNet,LSTM
- 归一化:梯度归一化,梯度裁剪
- 合理的权重初始和激活函数
权重初始化
Xavier初始
激活函数
总结
- 合理的权重初始值和激活函数的选取可以提升数值稳定性
15 实战:Kaggle房价预测+课程竞赛:加州2020年房价预测
15.1 实战:Kaggle房价预测
下载数据
使用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敏感)
调参
推理-提交预测