参考引用
- 动手学深度学习
线性回归基于几个简单的假设
举一个实际的例子:希望根据房屋的面积 (平方英尺) 和房龄 (年) 来估算房屋价格 (美元)
线性假设是指:目标 (房屋价格) 可以表示为特征 (面积和房龄) 的加权和
p r i c e = w a r e a ⋅ a r e a + w a g e ⋅ a g e + b \mathrm{price}=w_{\mathrm{area}}\cdot\mathrm{area}+w_{\mathrm{age}}\cdot\mathrm{age}+b price=warea⋅area+wage⋅age+b
给定一个数据集,目标是寻找模型的权重 w w w 和偏置 b b b,使得根据模型做出的预测大体符合数据里的真实价格。输出的预测值由输入特征通过线性模型的仿射变换决定,仿射变换由所选权重和偏置确定
而在机器学习领域,通常使用高维数据集,当输入包含 d d d 个特征时,预测结果 y ^ \hat{y} y^ (使用 “尖角” 符号表示 y y y 的估计值) 表示为
y ^ = w 1 x 1 + . . . + w d x d + b \hat{y}=w_1x_1+...+w_dx_d+b y^=w1x1+...+wdxd+b
将所有特征放到向量 x ∈ R d {\mathbf{x}}\in\mathbb{R}^{d} x∈Rd 中,并将所有权重放到向量 w ∈ R d {\mathbf{w}}\in\mathbb{R}^{d} w∈Rd 中,可以用点积形式来简洁地表达模型
y ^ = w ⊤ x + b \hat{y}=\mathbf{w}^\top\mathbf{x}+b y^=w⊤x+b
上式中,向量 x {\mathbf{x}} x 对应于单个数据样本的特征。用符号表示的矩阵 X ∈ R n × d \mathbf{X}\in\mathbb{R}^{n\times d} X∈Rn×d 可以很方便地引用整个数据集的 n n n 个样本。其中, X \mathbf{X} X 的每一行是一个样本,每一列是一种特征。对于特征集合 X \mathbf{X} X,预测值 y ^ ∈ R n \hat{\mathbf{y}}\in\mathbb{R}^{n} y^∈Rn 可以通过矩阵-向量乘法表示为
y ^ = X w + b \hat{\mathbf{y}}=\mathbf{X}\mathbf{w}+b y^=Xw+b
给定训练数据特征 X \mathbf{X} X 和对应的已知标签 y y y,线性回归的目标是找到一组权重向量 w w w 和偏置 b b b
在开始寻找最好的模型参数 (model parameters) w w w 和 b b b 之前,还需要两个东西
- (1) 一种模型质量的度量方式
- (2) 一种能够更新模型以提高模型预测质量的方法
在考虑如何用模型拟合数据之前,需要确定一个拟合程度的度量。损失函数 (loss function) 能够量化目标的实际值与预测值之间的差距。通常选择非负数作为损失,且数值越小表示损失越小,完美预测时的损失为 0。回归问题中最常用的损失函数是平方误差函数。当样本 i i i 的预测值为 y ^ ( i ) \hat{y}^{(i)} y^(i),其相应的真实标签为 y ( i ) y^{(i)} y(i) 时,平方误差可以定义为以下公式
l ( i ) ( w , b ) = 1 2 ( y ^ ( i ) − y ( i ) ) 2 l^{(i)}(\mathbf{w},b)=\frac12\left(\hat{y}^{(i)}-y^{(i)}\right)^2 l(i)(w,b)=21(y^(i)−y(i))2
由于平方误差函数中的二次方项,估计值 y ^ ( i ) \hat{y}^{(i)} y^(i) 和观测值 y ( i ) y^{(i)} y(i) 之间较大的差异将导致更大的损失。为了度景模型在整个数据集上的质量,需计算在训练集 n n n 个样本上的损失均值 (也等价于求和)
L ( w , b ) = 1 n ∑ i = 1 n l ( i ) ( w , b ) = 1 n ∑ i = 1 n 1 2 ( w ⊤ x ( i ) + b − y ( i ) ) 2 L(\mathbf{w},b)=\frac1n\sum_{i=1}^nl^{(i)}(\mathbf{w},b)=\frac1n\sum_{i=1}^n\frac12\left(\mathbf{w}^\top\mathbf{x}^{(i)}+b-y^{(i)}\right)^2 L(w,b)=n1i=1∑nl(i)(w,b)=n1i=1∑n21(w⊤x(i)+b−y(i))2
在训练模型时,希望寻找一组参数 ( w ∗ , b ∗ ) (\mathbf{w}^*,b^*) (w∗,b∗),这组参数能最小化在所有训练样本上的总损失
w ∗ , b ∗ = argmin w , b L ( w , b ) \mathbf{w}^*,b^*=\underset{\mathbf{w},b}{\operatorname*{argmin}}L(\mathbf{w},b) w∗,b∗=w,bargminL(w,b)
梯度下降 (gradient descent) 的方法几乎可以优化所有深度学习模型,它通过不断地在损失函数递减的方向上更新参数来降低误差
通常在每次需要计算更新的时候随机抽取一小批样本,这叫做小批量随机梯度下降(minibatch stochastic gradient descent)
B \mathcal{B} B 表示每个小批量中的样本数,这也称为批量大小 (batch size)。 η \eta η 表示学习率 (learning rate)。批量大小和学习率的值通常是手动预先指定,而不是通过模型训练得到的
import math
import time
import numpy as np
import torch
n = 10000
a = torch.ones([n])
b = torch.ones([n])
# 定义一个计时器
class Timer:
def __init__(self):
self.times = []
self.start()
def start(self):
self.tik = time.time()
def stop(self):
self.times.append(time.time() - self.tik)
return self.times[-1]
def avg(self):
return sum(self.times) / len(self.times)
def sum(self):
return sum(self.times)
def cumsum(self):
return np.array(self.times).cumsum().tolist()
# 使用 for 循环,每次执行一位的加法
c = torch.zeros(n)
timer = Timer()
for i in range(n):
c[i] = a[i] + b[i]
# 使用重载的 + 运算符来计算按元素的和
# 矢量化代码通常会带来数量级的加速
timer.start()
d = a + b
print(f'{timer.stop():.5f} sec')
# 输出
'0.20727 sec'
'0.00020 sec'
通过对噪声分布的假设来解读平方损失目标函数。正态分布 (normal distribution),也称为高斯分布 (Gaussian distribution):若随机变量 x x x 具有均值 μ \mu μ 和方差 σ 2 \sigma^{2} σ2 (标准差 σ \sigma σ),其正态分布概率密度函数如下
p ( x ) = 1 2 π σ 2 exp ( − 1 2 σ 2 ( x − μ ) 2 ) \begin{aligned}p(x)&=\frac1{\sqrt{2\pi\sigma^2}}\exp\left(-\frac1{2\sigma^2}(x-\mu)^2\right)\end{aligned} p(x)=2πσ21exp(−2σ21(x−μ)2)
均方误差损失函数 (简称均方损失) 可以用于线性回归的一个原因是:假设了观测中包含噪声,其中噪声服从正态分布。噪声正态分布如下式,其中 ϵ ∼ N ( 0 , σ 2 ) \epsilon\sim\mathcal{N}(0,\sigma^2) ϵ∼N(0,σ2)
y = w ⊤ x + b + ϵ y=\mathbf{w}^\top\mathbf{x}+b+\epsilon y=w⊤x+b+ϵ
因此,现在可以写出通过给定的 x \mathbf{x} x 观测到特定 y y y 的似然 (likelihood)
P ( y ∣ x ) = 1 2 π σ 2 exp ( − 1 2 σ 2 ( y − w ⊤ x − b ) 2 ) P(y\mid\mathbf{x})=\frac1{\sqrt{2\pi\sigma^2}}\exp\left(-\frac1{2\sigma^2}(y-\mathbf{w}^\top\mathbf{x}-b)^2\right) P(y∣x)=2πσ21exp(−2σ21(y−w⊤x−b)2)
现在,根据极大似然估计法,参数 w \mathbf{w} w 和 b b b 的最优值是使整个数据集的似然最大的值
P ( y ∣ X ) = ∏ i = 1 n p ( y ( i ) ∣ x ( i ) ) P(\mathbf{y}\mid\mathbf{X})=\prod_{i=1}^np(y^{(i)}|\mathbf{x}^{(i)}) P(y∣X)=i=1∏np(y(i)∣x(i))
− log P ( y ∣ X ) = ∑ i = 1 n 1 2 log ( 2 π σ 2 ) + 1 2 σ 2 ( y ( i ) − w ⊤ x ( i ) − b ) 2 -\log P(\mathbf{y}\mid\mathbf{X})=\sum_{i=1}^n\frac12\log(2\pi\sigma^2)+\frac1{2\sigma^2}\left(y^{(i)}-\mathbf{w}^\top\mathbf{x}^{(i)}-b\right)^2 −logP(y∣X)=i=1∑n21log(2πσ2)+2σ21(y(i)−w⊤x(i)−b)2
import math
import numpy as np
import matplotlib.pyplot as plt
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)
x = np.arange(-7, 7, 0.01)
# 改变均值会产生沿 x 轴的偏移,增加方差将会分散分布、降低峰值
params = [(0, 1), (0, 2), (3, 1)]
plt.figure(figsize=(8, 6))
for mu, sigma in params:
plt.plot(x, normal(x, mu, sigma), label=f'mean {mu}, std {sigma}')
plt.xlabel('x')
plt.ylabel('p(x)')
plt.legend()
plt.show()
下图所示的神经网络中
输入值都是已经给定的,并且只有一个计算神经元。由于模型重点在发生计算的地方,所以通常在计算层数时不考虑输入层。也就是说,下图中神经网络的层数为 1
可以将线性回归模型视为仅由单个人工神经元组成的神经网络,或称为单层神经网络。对于线性回归,每个输入都与每个输出(在本例中只有一个输出)相连,将这种变换 (图中的输出层) 称为全连接层 (fully-connected laver) 或称为稠密层 (dense laver)
import numpy as np
import torch
from torch.utils import data
def synthetic_data(w, b, num_examples):
X = torch.normal(0, 1, (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)
def load_array(data_arrays, batch_size, is_train=True):
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)
# 为了验证是否正常工作,读取并打印第一个小批量样本
# 使用 iter 构造 Python 迭代器,并使用 next 从迭代器中获取第一项
print(next(iter(data_iter)))
# 输出
[tensor([[ 1.0829, -0.0883],
[ 0.0989, 0.7460],
[ 1.0245, -0.1956],
[-0.7932, 1.7843],
[ 1.2336, 1.0276],
[ 2.1166, 0.2072],
[-0.1430, 0.4944],
[ 0.7086, 0.3950],
[-0.0851, 1.4635],
[ 0.2977, 1.8625]]),
tensor([[ 6.6616],
[ 1.8494],
[ 6.9229],
[-3.4516],
[ 3.1747],
[ 7.7283],
[ 2.2302],
[ 4.2612],
[-0.9383],
[-1.5352]])]
from torch import nn
net = nn.Sequential(nn.Linear(2, 1))
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}')
import numpy as np
import torch
from torch.utils import data
from torch import nn
# 生成数据集
def synthetic_data(w, b, num_examples):
X = torch.normal(0, 1, (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)
# 读取数据集
def load_array(data_arrays, batch_size, is_train=True):
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)
# 定义模型
net = nn.Sequential(nn.Linear(2, 1))
# 初始化模型参数
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}')
w = net[0].weight.data
print('w的估计误差:', true_w - w.reshape(true_w.shape))
b = net[0].bias.data
print('b的估计误差:', true_b - b)
# 输出
epoch 1, loss 0.000216
epoch 2, loss 0.000104
epoch 3, loss 0.000102
w的估计误差: tensor([-0.0002, 0.0004])
b的估计误差: tensor([0.0002])
为估计所有可能类别的条件概率,需要一个有多个输出的模型,每个类别对应一个输出。为了解决线性模型的分类问题,需要和输出一样多的仿射函数。每个输出对应于它自己的仿射函数。本例中有 4 个特征和 3 个可能的输出类别,因此将需要 12 个标量来表示权重 (带下标的 w w w),3 个标量来表示偏置 (带下标的 b b b)。下面为每个输入计算三个未规范化的预测 (logit): o 1 , o 2 和 o 3 o_1,o_2\text{和}o_3 o1,o2和o3
o 1 = x 1 w 11 + x 2 w 12 + x 3 w 13 + x 4 w 14 + b 1 , o 2 = x 1 w 21 + x 2 w 22 + x 3 w 23 + x 4 w 24 + b 2 , o 3 = x 1 w 31 + x 2 w 32 + x 3 w 33 + x 4 w 34 + b 3 . \begin{aligned}o_1&=x_1w_{11}+x_2w_{12}+x_3w_{13}+x_4w_{14}+b_1,\\o_2&=x_1w_{21}+x_2w_{22}+x_3w_{23}+x_4w_{24}+b_2,\\o_3&=x_1w_{31}+x_2w_{32}+x_3w_{33}+x_4w_{34}+b_3.\end{aligned} o1o2o3=x1w11+x2w12+x3w13+x4w14+b1,=x1w21+x2w22+x3w23+x4w24+b2,=x1w31+x2w32+x3w33+x4w34+b3.
可以用神经网络图描述这个计算过程。与线性回归一样,softmax 回归也是一个单层神经网络由于计算每个输出 o 1 , o 2 和 o 3 o_1,o_2\text{和}o_3 o1,o2和o3 取决于所有输入 x 1 , x 2 , x 3 和 x 4 x_{1},x_{2},x_{3}\text{和}x_{4} x1,x2,x3和x4,所以 softmax 回归的输出层也是全连接层
softmax 函数能够将未规范化的预测变换为非负数并且总和为 1,同时让模型保持可导的性质。为了完成这一目标,首先对每个未规范化的预测求幂,这样可以确保输出非负。为了确保最终输出的概率值总和为 1,再让每个求幂后的结果除以它们的总和
y ^ = s o f t m a x ( o ) 其中 y ^ j = exp ( o j ) ∑ k exp ( o k ) \hat{\mathbf{y}}=\mathrm{softmax}(\mathbf{o})\quad\text{其中}\quad\hat{y}_j=\frac{\exp(o_j)}{\sum_k\exp(o_k)} y^=softmax(o)其中y^j=∑kexp(ok)exp(oj)
尽管softmax是一个非线性函数,但 softmax 回归的输出仍然由输入特征的仿射变换决定。因此 softmax 回归是一个线性模型
信息论 (information theory) 涉及编码、解码、发送以及尽可能简洁地处理信息或数据
信息论的核心思想是量化数据中的信息内容,该数值被称为分布 P P P 的熵 (entropy)
H [ P ] = ∑ j − P ( j ) log P ( j ) H[P]=\sum_j-P(j)\log P(j) H[P]=j∑−P(j)logP(j)
import torch
import torchvision
from torch.utils import data
from torchvision import transforms
import matplotlib.pyplot as plt
# 通过 ToTensor 实例将图像数据从 PIL 类型变换成 32 位浮点数格式
# 并除以 255 使得所有像素的数值均在 0~1 之间
trans = transforms.ToTensor()
# root:指定数据集下载或保存的路径;train:指定加载的是训练数据集还是测试数据集
# transform:指定数据集的转换操作;download:指定是否下载数据集
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)
# 将标签转换成对应的类别名称
def get_fashion_mnist_labels(labels):
text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat',
'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']
# 这是一个列表推导式
# 1.将 labels 中的每个元素按照索引转换为对应的文本标签
# 2.然后将这些元素组成一个新的列表并返回
return [text_labels[int(i)] for i in labels]
def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5):
figsize = (num_cols * scale, num_rows * scale)
# 第一个变量_是一个通用变量名,通常用于表示一个不需要使用的值
# 第二个变量 axes 是一个包含所有子图对象的数组
# 这里使用这种命名约定是为了表示只关心 axes 而不关心第一个返回值
_, axes = plt.subplots(num_rows, num_cols, figsize=figsize)
axes = axes.flatten() # 将 axes 展平为一维数组
# 遍历 axes 和 imgs 的元素,其中 i 为索引,ax 为当前子图,img 为当前图像
for i, (ax, img) in enumerate(zip(axes, imgs)):
if isinstance(img, torch.Tensor): # img 是一个 torch.Tensor 类型
# img 是一个张量,假设其形状为 (C, H, W),其中 C 代表通道数,H 代表高度,W 代表宽度
# permute(1, 2, 0) 是对 img 进行维度重排操作。它将维度从 (C, H, W) 重排为 (H, W, C)
ax.imshow(img.permute(1, 2, 0))
else:
ax.imshow(img)
ax.axis('off') # 关闭图像的坐标轴
if titles:
ax.set_title(titles[i])
plt.show()
X, y = next(iter(data.DataLoader(mnist_train, batch_size=18)))
show_images(X, 2, 9, titles=get_fashion_mnist_labels(y))
batch_size = 256
def get_dataloader_workers():
return 4 # 使用 4 个进程来读取数据
train_iter = data.DataLoader(mnist_train, batch_size, shuffle=True,
num_workers=get_dataloader_workers())
def load_data_fashion_mnist(batch_size, resize=None):
# 下载 Fashion-MNIST 数据集,然后将其加载到内存中
trans = [transforms.ToTensor()]
if resize:
trans.insert(0, transforms.Resize(resize))
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()))
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
# 设置随机种子以确保结果可重复
torch.manual_seed(42)
# 定义超参数
batch_size = 128 # 每个批次的样本数
learning_rate = 0.1 # 学习率,用于控制优化过程中参数更新的步长
num_epochs = 100 # 训练的轮数
# 加载 Fashion-MNIST 数据集
transform = transforms.Compose([
transforms.ToTensor(), # 将图像转换为张量
transforms.Normalize((0.5,), (0.5,)) # 将像素值归一化到 [-1,1] 区间
])
# 加载训练集和测试集,并将数据转换为张量
train_dataset = torchvision.datasets.FashionMNIST(root='./data', train=True, download=True, transform=transform)
test_dataset = torchvision.datasets.FashionMNIST(root='./data', train=False, download=True, transform=transform)
# 创建训练集和测试集的数据加载器,用于批量获取数据
train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=False)
# 定义模型
# 创建了一个名为 SoftmaxRegression 的类,继承自 nn.Module
class SoftmaxRegression(nn.Module):
def __init__(self, input_size, num_classes): # 构造函数 init 初始化
super(SoftmaxRegression, self).__init__()
# 定义了一个线性层 (nn.Linear) 作为模型的唯一层次结构
# 输入大小为 input_size,输出大小为 num_classes
self.linear = nn.Linear(input_size, num_classes)
# 实现了前向传播操作,将输入数据通过线性层得到输出
def forward(self, x):
out = self.linear(x)
return out
model = SoftmaxRegression(input_size=784, num_classes=10)
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss() # 用于计算多分类问题中的交叉熵损失
optimizer = optim.SGD(model.parameters(), lr=learning_rate) # 定义随机梯度下降优化器,用于更新模型的参数
# 训练模型
train_losses = []
test_losses = []
# 在模型训练的过程中,运行模型对全部数据完成一次前向传播和反向传播的完整过程叫做一个 epoch
# 在梯度下降的模型训练的过程中,神经网络逐渐从不拟合状态到优化拟合状态,达到最优状态之后会进入过拟合状态
# 因此 epoch 并非越大越好。数据越多样,相应 epoch 就越大
for epoch in range(num_epochs):
train_loss = 0.0
# 1.将模型设置为训练模式
model.train()
for images, labels in train_loader:
# 将输入数据展平
images = images.reshape(-1, 784)
# 前向传播、计算损失、反向传播和优化
outputs = model(images)
loss = criterion(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss += loss.item()
# 2.将模型设置为评估模式(在测试集上计算损失)
model.eval()
test_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
for images, labels in test_loader:
images = images.reshape(-1, 784)
outputs = model(images)
loss = criterion(outputs, labels)
test_loss += loss.item()
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
train_loss /= len(train_loader)
test_loss /= len(test_loader)
accuracy = 100 * correct / total
train_losses.append(train_loss)
test_losses.append(test_loss)
print(f'Epoch [{epoch + 1}/{num_epochs}], Train Loss: {train_loss:.4f}, Test Loss: {test_loss:.4f}, Accuracy: {accuracy:.2f}%')
# 可视化损失
plt.plot(train_losses, label='Train Loss')
plt.plot(test_losses, label='Test Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
plt.show()
# 输出
Epoch [1/100], Train Loss: 0.6287, Test Loss: 0.5182, Accuracy: 81.96%
Epoch [2/100], Train Loss: 0.4887, Test Loss: 0.4981, Accuracy: 82.25%
Epoch [3/100], Train Loss: 0.4701, Test Loss: 0.4818, Accuracy: 82.49%
Epoch [4/100], Train Loss: 0.4554, Test Loss: 0.4719, Accuracy: 82.90%
Epoch [5/100], Train Loss: 0.4481, Test Loss: 0.4925, Accuracy: 82.57%
Epoch [6/100], Train Loss: 0.4360, Test Loss: 0.4621, Accuracy: 83.53%
Epoch [7/100], Train Loss: 0.4316, Test Loss: 0.4662, Accuracy: 83.53%
Epoch [8/100], Train Loss: 0.4293, Test Loss: 0.4543, Accuracy: 83.80%
Epoch [9/100], Train Loss: 0.4289, Test Loss: 0.5460, Accuracy: 81.09%
...