线性回归
线性回归(linear regression)可以追溯到19世纪初,它在回归的各种标准⼯具中最简单且最流⾏。
线性回归基于几个简单的假设:⾸先,假设⾃变量x和因变量y之间的关系是线性的,即y可以表⽰为x中元素的加权和,这⾥通常允许包含观测值的⼀些噪声;其次,假设任何噪声都比较正常,如噪声遵循正态分布。
假如我们希望根据房屋的⾯积(平⽅英尺)和房龄(年)来估算房屋价格(美元)。为了开发⼀个能预测房价的模型,我们需要收集⼀个真实的数据集。这个数据集包括了房屋的销售价格、⾯积和房龄。在机器学习的术语中,该数据集称为训练数据集(training dataset)或训练集(training set)。每⾏数据(比如⼀次房屋交易相对应的数据)称为样本(sample),也可以称为数据点(datapoint)或数据样本(data instance)。我们把试图预测的⽬标(比如预测房屋价格)称为标签(label)或⽬标(target)。预测所依据的⾃变量(⾯积和房龄)称为特征(feature)或协变量(covariate)。
通常,使用n来表示数据集中的样本数。对索引为i的样本,其输入表⽰为x(i)=[x1(i),x2(i)]⊤mathbf{x}^{(i)}=left[x_{1}^{(i)}, x_{2}^{(i)}right]^{top},对应的标签是y(i)y^{(i)}
线性模型
线性假设是指⽬标(房屋价格)可以表⽰为特征(⾯积和房龄)的加权和
price=warea ⋅area+wage ⋅age+bprice =w_{text {area }} cdot area +w_{text {age }} cdot age +b
权重决定了每个特征对预测值的影响。b称为偏置(bias)、偏移量(offset)或截距(intercept)。偏置是指当所有特征都取值为0时,预测值应该为多少。即使现实中不会有任何房⼦的⾯积是0或房龄正好是0年,仍然需要偏置项。如果没有偏置项,模型的表达能⼒将受到限制。
在机器学习领域通常使用的是⾼维数据集,建模时采用线性代数表⽰法会比较⽅便。当输入包含d个特征时,将预测结果y^hat{y} 表⽰为
y^=w1x1+…+wdxd+bhat{y}=w_{1} x_{1}+ldots+w_{d} x_{d}+b
将权重使用特征矩阵表示:
y^=w⊤x+b.hat{y}=mathbf{w}^{top} mathbf{x}+b.
线性回归的⽬标是找到⼀组权重向量w和偏置b:当给定从X的同分布中取样的新样本特征时,这组权重向量和偏置能够使得新样本预测标签的误差尽可能小。
线性回归是⼀个单层神经⽹络
torch自定义简单线性回归
使用torch实现一个简单的线性回归,首先假设有一个已经真实存在的线性函数,使用线性模型参数w=[2,−3.4]Tw=[2, −3.4]^T、b = 4.2 和噪声项ϵ⽣成数据集及其标签:
y=Xw+b+ϵmathbf{y}=mathbf{X} mathbf{w}+b+epsilon
根据此函数生成一个1000样本的数据集
import torch
def synthetic_data(w, b, num_examples):
"""⽣成y=Xw+b+噪声"""
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))
X,y组成一个完整数据集,X是特征,y是标签
true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)
对数据集进行可视化观察,特征与目标值之间的关系
import matplotlib.pyplot as plt
plt.scatter(features[:, (0)].detach().numpy(), labels.detach().numpy(),1)
plt.scatter(features[:, (1)].detach().numpy(), labels.detach().numpy(),1)
训练神经网络之前需要随机初始化权重并将偏置初始化为0。
w = torch.normal(0, 0.01, size=(2,1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
w
tensor([[ 0.0092],
[-0.0167]], requires_grad=True)
在初始化参数之后,训练模型任务就是更新这些参数,直到这些参数⾜够拟合数据。每次更新都需要计算损失函数关于模型参数的梯度。有了这个梯度,就可以向减小损失的⽅向更新每个参数。
接下来,必须定义模型,将模型的输入和参数同模型的输出关联起来,在这个简单的线性模型中,只需要输入特征矩阵X,传入要训练的权重矩阵w与偏置b即可
def linreg(X, w, b):
"""线性回归模型"""
return torch.matmul(X, w) + b
因为需要计算损失函数的梯度,所以应该先定义损失函数。这里使用平方损失函数。
def squared_loss(y_hat, y):
"""均⽅损失"""
return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
最后定义优化算法更新权重,这里使用随机梯度下降sgd
def sgd(params, lr, batch_size):
"""小批量随机梯度下降"""
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_()
现在已经准备好了模型训练所有需要的要素,可以实现主要的训练过程部分了。在每次迭代中,读取⼀小批量训练样本,并通过模型来获得⼀组预测。计算完损失后开始反向传播,存储每个参数的梯度。最后,调用优化算法sgd
来更新模型参数。
- 初始化参数
- 每个迭代周期(epoch)重复以下训练,直到完成
- 计算梯度
- 更新参数(w, b) ← (w, b)
训练模型时要对数据集进行遍历,每次epoch
对全部样本随机抽取批量样本,并使用它们来更新模型
import random
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
)等
batch_size = 10
lr = 0.03
num_epochs = 3
net = linreg
loss = squared_loss
for epoch in range(num_epochs): # 迭代批次(epoch)
for X, y in data_iter(batch_size, features, labels): # 每次迭代根据batch大小随机训练多轮数据
l = loss(net(X, w, b), y) # 计算损失函数,X和y的小批量损失
l.sum().backward() # 因为l形状是(batch_size,1),⽽不是⼀个标量。l中的所有元素被加到⼀起,并以此计算关于[w,b]的梯度
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}')
epoch 1, loss 14.806926
epoch 1, loss 13.858301
...
epoch 3, loss 0.000052
epoch 3, loss 0.000051
可以看到经过多批次(epoch),每批次随机抽取样本进行多轮训练,最终损失函数收敛到一定值
因为使用的是自己合成的数据集,所以知道真正的参数是什么。因此可以通过比较真实参数和通过训练学到的参数来评估训练的成功程度
print(f'w的估计误差: {true_w - w.reshape(true_w.shape)}')
print(f'b的估计误差: {true_b - b}')
w的估计误差: tensor([ 0.0010, -0.0014], grad_fn=<SubBackward0>)
b的估计误差: tensor([0.0002], grad_fn=<RsubBackward1>)
torch简单线性回归
使用torch
的API实现上述简单的线性回归,首先加载数据迭代器
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)
定义模型
对于标准深度学习模型,可以使用框架的预定义好的层。这使我们只需关注使用哪些层来构造模型,⽽不必关注层的实现细节。
⾸先定义⼀个模型变量net,它是⼀个Sequential类的实例。Sequential类将多个层串联在⼀起。当给定输入数据时,Sequential实例将数据传入到第⼀层,然后将第⼀层的输出作为第⼆层的输入,以此类推。
在PyTorch中,全连接层在Linear类中定义,我们将两个参数传递到nn.Linear中。第⼀个指定输入特征形状,即2,第⼆个指定输出特征形状,输出特征形状为单个标量,因此为1。
# nn是神经⽹络的缩写
from torch import nn
net = nn.Sequential(nn.Linear(2, 1))
初始化模型参数
在使用net之前,需要初始化模型参数。如在线性回归模型中的权重和偏置。在这⾥,我们指定每个权重参数应该从均值为0、标准差为0.01的正态分布中随机采样,偏置参数将初始化为零。
正如我们在构造nn.Linear
时指定输入和输出尺⼨⼀样,现在我们能直接访问参数以设定它们的初始值。我们通过net[0]
选择⽹络中的第⼀个图层,然后使用weight.data
和bias.data
⽅法访问参数。
深度学习框架通常有预定义的方法来初始化参数,所以一般不需要手动随机初始化模型参数
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)
定义损失函数
计算均方误差使用的是MSELoss类,也称为平方L2范数。默认情况下,它返回所有样本损失的平均值。
loss = nn.MSELoss()
定义优化算法
小批量随机梯度下降算法是⼀种优化神经网络的标准⼯具,PyTorch
在optim
模块中实现了该算法的许多变种。当实例化⼀个SGD实例时,指定优化的参数(可通过net.parameters()从模型中获得)以及优化算法所需的超参数字典。小批量随机梯度下降只需要设置lr值,这⾥设置为0.03。
trainer = torch.optim.SGD(net.parameters(), lr=0.03)
训练模型
之后训练神经网络,对于已定义好的模型一般直接传入模型参数,损失函数、优化器等超参数直接训练即可
for epoch in range(num_epochs):
for X, y in load_array((features, labels), batch_size):
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}')
epoch 1, loss 0.000418
epoch 2, loss 0.000102
epoch 3, loss 0.000103
评估训练误差
w = net[0].weight.data
print('w的估计误差:', true_w - w.reshape(true_w.shape))
b = net[0].bias.data
print('b的估计误差:', true_b - b)
w的估计误差: tensor([ 0.0017, -0.0006])
b的估计误差: tensor([-0.0004])
分类问题
从⼀个图像分类问题开始。假设每次输入是⼀个2 × 2的灰度图像。我们可以用⼀个标量表⽰每个像素值,每个图像对应四个特征x1, x2, x3, x4。此外,假设每个图像属于类别“猫”,“鸡”和“狗”中的⼀个。接下来,我们要选择如何表⽰标签。有两个明显的选择:最直接的想法是选择y ∈ {1, 2, 3},其中整数分别代表{狗, 猫, 鸡}。这是在计算机上存储此类信息的有效⽅法。
如果类别间有⼀些⾃然顺序,⽐如说我们试图预测{婴儿, 儿童, ⻘少年, ⻘年人, 中年人, ⽼年人},那么将这个问题转变为回归问题,并且保留这种格式是有意义的。但是⼀般的分类问题并不与类别之间的⾃然顺序有关。幸运的是,统计学家很早以前就发明了⼀种表⽰分类数据的简单⽅法:独热编码(one-hot encoding)。
独热编码
独热编码是⼀个向量,它的分量和类别⼀样多。类别对应的分量设置为1,其他所有分量设置为0。在上述例⼦中,标签y将是⼀个三维向量,其中(1, 0, 0)对应于“猫”、(0, 1, 0)对应于“鸡”、(0, 0, 1)对应于“狗”:
y∈(1,0,0),(0,1,0),(0,0,1)y ∈ {(1, 0, 0),(0, 1, 0),(0, 0, 1)}
线性模型分类
为了估计所有可能类别的条件概率,需要⼀个有多个输出的模型,每个类别对应⼀个输出。为了解决线性模型的分类问题,需要和输出⼀样多的仿射函数(affine function)。每个输出对应于它⾃⼰的仿射函数。在上述例⼦中,由于有4个特征和3个可能的输出类别,需要12个标量来表⽰权重(带下标的w),3个标量来表⽰偏置(带下标的b)
o1=x1w11+x2w12+x3w13+x4w14+b1,o2=x1w21+x2w22+x3w23+x4,w24+b2o3=x1w31+x2w32+x3w33+x4w34+b3.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}.
torch简单Minist分类
MNIST数据集 [LeCun et al., 1998] 是图像分类中⼴泛使用的数据集之⼀,但作为基准数据集过于简单。这里使用类似但更复杂的Fashion-MNIST数据集 [Xiao et al., 2017]。Fashion-MNIST由10个类别的图像组成,每个类别由训练数据集(train dataset)中的6000张图像和测试数据 集(test dataset)中的1000张图像组成。因此,训练集和测试集分别包含60000和10000张图像。
import torchvision
from torchvision import transforms
trans = transforms.ToTensor()
mnist_train = torchvision.datasets.FashionMNIST(root="../datasets/minist", train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(root="../datasets/minist", train=False, transform=trans, download=True)
len(mnist_train), len(mnist_test)
# (60000, 10000)
mnist_train[0][0].shape
# torch.Size([1, 28, 28])
Fashion-MNIST中包含的10个类别,分别为t-shirt( T恤)、trouser(裤⼦)、pullover(套衫)、dress(连⾐ 裙)、coat(外套)、sandal(凉鞋)、shirt(衬衫)、sneaker(运动鞋)、bag(包)和ankle boot(短靴)。
def get_fashion_mnist_labels(labels):
"""返回Fashion-MNIST数据集的⽂本标签"""
text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat','sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']
return [text_labels[int(i)] for i in labels]
可视化样本
def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5):
"""绘制图像列表"""
figsize = (num_cols * scale, num_rows * scale)
_, axes = plt.subplots(num_rows, num_cols, figsize=figsize)
axes = axes.flatten()
for i, (ax, img) in enumerate(zip(axes, imgs)):
if torch.is_tensor(img):
# 图⽚张量
ax.imshow(img.numpy())
else:
# PIL图⽚
ax.imshow(img)
ax.axes.get_xaxis().set_visible(False)
ax.axes.get_yaxis().set_visible(False)
if titles:
ax.set_title(titles[i])
return axes
X, y = next(iter(data.DataLoader(mnist_train, batch_size=18)))
show_images(X.reshape(18, 28, 28), 2, 9, titles=get_fashion_mnist_labels(y));
在每次迭代中,数据加载器每次都会读取⼀小批量数据,⼤小为batch_size。通过内置数据迭代器随机打乱了所有样本,从⽽⽆偏⻅地读取⼩批量。
batch_size = 256
train_iter = data.DataLoader(mnist_train, batch_size, shuffle=True,num_workers=4)
test_iter = data.DataLoader(mnist_test, batch_size, shuffle=True,num_workers=4)
定义模型
只需在Sequential中添加⼀个带有10个输出的全连接层,在这⾥Sequential并不是必要的,但它是实现深度模型的基础。我们仍然以均值0和标准差0.01随机初始化权重。
# 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)
net.apply(init_weights);
选择损失函数
多分类激活函数一般是softmax
,损失函数一般选择交叉熵损失函数
loss = nn.CrossEntropyLoss(reduction='none')
选择优化器
使用学习率为0.1的⼩批量随机梯度下降作为优化算法
optimizer = torch.optim.SGD(net.parameters(), lr=0.1)
训练模型
定义模型评价指标并训练模型
def train_minist(model, train_iter, loss, num_epochs, optimizer):
"""训练模型"""
for epoch in range(num_epochs):
model.train() # 将模型设置为训练模式
for X, y in train_iter:
y_predict = model(X) # 计算梯度并更新参数
l = loss(y_predict, y) # 计算损失函数
optimizer.zero_grad() # 梯度清零
l.mean().backward() # 反向传播
optimizer.step() # 梯度更新
num_epochs = 10
train_minist(net, train_iter, loss, num_epochs, optimizer)
from sklearn.metrics import accuracy_score
trues = []
preds = []
for X, y in test_iter:
trues.extend(get_fashion_mnist_labels(y))
preds.extend(get_fashion_mnist_labels(net(X).argmax(axis=1)))
print(accuracy_score(trues,preds))
0.8315
一个简单模型就有0.8315准确率