#深度学习# #人工智能#
3. 线性神经网络
经典统计学习技术中的线性回归和softmax回归可以视为线性神经网络。
3.1线性回归
3.1.1 线性回归的基本元素
线性问题。
例子:房价预测
假设 1:
影响房价的关键因素:
卧室数目,卫浴数目和房子大小,分别用𝑥1, 𝑥2, 𝑥3表示
假设 2:
销售价格是关键因素的加权总和:𝑦 = 𝑤1𝑥1+𝑤2𝑥2+𝑤3𝑥3+𝑏 (w,b为权重和偏差,后续确定)
损失函数(此处有的形式在括号前有常数1/2,不会有本质区别,有1/2时,当我们对损失函数求导后常数系数为1):
训练数据集:包含真实值x和与y
学习参数:
由于是线性函数,因此具有显式解:(线性回归可以看作是单层神经网络)
梯度下降:
注意学习率不能太小,效率低,成本高;也不能太大,太大会导致一直震荡,并不是真正的下降:如图所示:
梯度下降最简单的用法是计算损失函数(数据集中所有样本的损失均值),关于模型参数的导数(梯度)。实际中的执行可能会非常慢:因为在每一次更新参数之前,我们必须遍历整个数据集。
小批量随机梯度下降:每次需要计算更新的时候随机抽取一小批样本b(batch-size)。
超参数(hyperparameter)名词补充说明:
b:批量大小(batch size); η:表示学习率(learning rate)
调参(hyperparameter tuning)是选择超参数的过程。批量大小和学习率的值通常是手动预先指定,而不是通过模型训练得到的。
批量大小的选择注意事项:
3.1.2 矢量化加速
训练我们的模型时,我能够同时处理整个小批量的样本。矢量化代码通常会带来数量级的加速。
例子:
#导入相关工具包
import math
import time
import numpy as np
import torch
#定义一个计时器
class Timer: #@save
"""记录多次运行时间"""
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()
#初始化生成相应向量
n = 10000
a = torch.ones([n])
b = torch.ones([n])
a ,b
结构如下:
验证阶段:1.使用for循环,每次执行一位的加法。
c = torch.zeros(n)
timer = Timer()
for i in range(n):
c[i] = a[i] + b[i]
f'{timer.stop():.5f} sec'
结果:
2.使用重载的+运算符来计算按元素的和。
timer.start()
d = a + b
f'{timer.stop():.5f} sec'
结果:
3.1.3 正态分布与平方损失
正态分布(normal distribution),也称为高斯分布(Gaussiandistribution)。随机变量x具有均值µ和方差σ2(标准差σ),其正态分布概率密度函数如下:
def normal(x, mu, sigma):
p = 1 / math.sqrt(2 * math.pi * sigma**2)
return p * np.exp((-1 / 2*sigma**2) * (x - mu)**2)
可视化:
from d2l import torch as d2l
# 再次使用numpy进行可视化
x = np.arange(-7, 7, 0.01)
# 均值和标准差对
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])
得出:改变均值会产生沿x轴的偏移,增加方差将会分散分布、降低其峰值。
3.2 线性回归的从零开始实现
从零开始实现整个方法,包括数据流水线、模型、损失函数和小批量随机梯度下降优化器。
导入工具包:
%matplotlib inline
import random
import torch
from d2l import torch as d2l
3.2.1 生成数据集
根据带有噪声的线性模型构造一个人造数据集(好处:知道真实的w和b):
def synthetic_data(w, b, num_examples): #样本数num_examples
"""生成y=Xw+b+噪声"""
X = torch.normal(0, 1, (num_examples, len(w))) #均值为0,方差为1,num_examples样本,列数为w的长度。
y = torch.matmul(X, w) + b #x*w+b
y += torch.normal(0, 0.01, y.shape) #随机噪音,均值为0方差为0.01,形状同y
return X, y.reshape((-1, 1)) #x和y作为列向量返回
true_w = torch.tensor([2, -3.4]) #定义真实的w
true_b = 4.2 #定义真实的b
features, labels = synthetic_data(true_w, true_b, 1000) #生成特征和标注
print('features:', features[0],'\nlabel:', labels[0]) #打印第0个样本的情况,输出结果显示为长为2的向量;标签为一个标量
d2l.set_figsize()
d2l.plt.scatter(features[:, 1].detach().numpy(), labels.detach().numpy(), 1) #将第一列和标签作图
结果:
3.2.2 读取数据集
定义一个data_iter函数,该函数接收批量大小、特征矩阵和标签向量作为输入,生成大小为batch_size的小批量。每个小批量包含一组特征和标签。
def data_iter(batch_size, features, labels):
num_examples = len(features) #样本数量等于特征矩阵长度
indices = list(range(num_examples)) #生成索引list,0到num_examples
# 这些样本是随机读取的,没有特定的顺序
random.shuffle(indices) #随机打乱下标,利用随机数据去访问样本
for i in range(0, num_examples, batch_size): #从0开始,一直到num_examples,每次条batch_size个大小。
batch_indices = torch.tensor(
indices[i: min(i + batch_size, num_examples)]) #从i开始,到i+batch_size,到num_examples,如果超出num_examples,则取min值
yield features[batch_indices], labels[batch_indices] #yield相当于返回函数,一直调用,一直返回值,直至结束。
利用上述定义寒素,读取第一个小批量数据样本并打印。每个批量的特征维度显示批量大小和输入特征数。同样的,批量的标签形状与batch_size相等。
batch_size = 10
for X, y in data_iter(batch_size, features, labels):
print(X, '\n', y)
break
结果:
3.2.3 初始化模型参数
生成均值为0、标准差为0.01的正态分布中采样随机数来初始化权重,并将偏置初始化为0。
w = torch.normal(0, 0.01, size=(2,1), requires_grad=True) #长为2的一个向量,requires_grad=True表示需要计算梯度进行更新
b = torch.zeros(1, requires_grad=True) #偏差为一个标量
w,b
结果:
3.2.4 定义模型
计算线性模型的输出,我们只需计算输入特征X和模型权重w的矩阵‐向量乘法后加上偏置b。注意,上面的Xw是一个向量,而b是一个标量。基于广播机制:一个向量加一个标量时,标量会被加到向量的每个分量上。
def linreg(X, w, b): #@save
"""线性回归模型"""
return torch.matmul(X, w) + b
3.2.5 定义损失函数
计算损失函数的梯度,所以我们应该先定义损失函数。在实现中,需要将真实值y的形状转换为和预测值y_hat的形状相同。
def squared_loss(y_hat, y): #@save
"""均方损失"""
return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
3.2.6 定义优化算法
def sgd(params, lr, batch_size): #params参数是一个list 包含我w,b;lr学习率,批大学batch_size
"""小批量随机梯度下降"""
with torch.no_grad():#在优化期间不计算梯度
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_()#手动对梯度至0
3.2.7 训练
lr = 0.03
num_epochs = 6
net = linreg
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) # X和y的小批量损失,#将x,w,b做预测,然后与真实y作损失
# 因为l形状是(batch_size,1),而不是一个标量。l中的所有元素被加到一起,
# 并以此计算关于[w,b]的梯度
l.sum().backward() #求和,之后计算梯度(反向传播)
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}')
结果:
比较真实参数和通过训练学到的参数来评估训练的成功程度。事实上,真实参数和通过训练学到的参数确实非常接近。
3.3 线性回归的简洁实现
3.3.1 生成数据集
import numpy as np
import torch
from torch.utils import data
from d2l import torch as d2l
true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = d2l.synthetic_data(true_w, true_b, 1000)
3.3.2 读取数据集
通过调用框架中现有的API来读取数据。我们将features和labels作为API的参数传递,并通过数据迭代器指定batch_size。
def load_array(data_arrays, batch_size, is_train=True): #@save
"""构造一个PyTorch数据迭代器"""
dataset = data.TensorDataset(*data_arrays) #将features, labels做为list,传入TensorDataset,得到dataset。
return data.DataLoader(dataset, batch_size, shuffle=is_train) #调用DataLoader函数,每次从dataset中随机提取batch_size个样本,shuffle=is_train随机打乱顺序
batch_size = 10
data_iter = load_array((features, labels), batch_size)
#使用iter构造Python迭代器,并使用next从迭代器中获取第一项。
next(iter(data_iter))
结果:
3.3.3 定义模型
使用框架的预定义好的层。首先定义一个模型变量net,它是一个Sequential类的实例(一个容器)。Sequential类将多个层串联在一起。当给定输入数据时,Sequential实例将数据传入到第一层,然后将第一层的输出作为第二层的输入,以此类推。
# nn是神经网络的缩写
from torch import nn
net = nn.Sequential(nn.Linear(2, 1))#输入特征形状2,输出特征形状为1
3.3.4 初始化模型参数
指定每个权重参数应该从均值为0、标准差为0.01的正态分布中随机采样,偏置参数将初始化为零。
通过0访问linear,weight访问w,normal使用正态分布来替换data的值。
net[0].weight.data.normal_(0, 0.01)#均值为0,方差为0.01
net[0].bias.data.fill_(0)#bias表偏差,fill填充为0
结果:
3.3.5 定义损失函数
均方误差使用的是MSELoss类,也称为平方L2范数。默认情况下,它返回所有样本损失的平均值。
loss = nn.MSELoss()
3.3.6 定义优化算法
小批量随机梯度下降算法:
trainer = torch.optim.SGD(net.parameters(), lr=0.03) #net.parameters()包括了w,b等参数
3.3.7 训练
num_epochs = 3 #迭代轮数
for epoch in range(num_epochs):
for X, y in data_iter:#在data_iter里取出x,y
l = loss(net(X) ,y)
trainer.zero_grad()#优化器梯度清零
l.backward()#pytorch已经做了sum,所以不用sum,直接计算梯度
trainer.step()#调用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)
3.4 softmax回归
回归估计连续值,分类预测离散类别
1.softmax运算获取一个向量并将其映射为概率。
2.softmax回归适用于分类问题,它使用了softmax运算中输出类别的概率分布。
3.交叉熵是一个衡量两个概率分布之间差异的很好的度量,它测量给定模型编码数据所需的比特数
从回归到多类分类问题-均方损失:
从回归到多类分类问题-无校验比例:
需要使得正确类的置信度Oy远远大于非正确类的置信度Oi,为一个阈值。
3.5 图像分类数据集
将使用类似但更复杂的Fashion‐MNIST数据集。Fashion‐MNIST由10个类别的图像组成,每个类别由训练数据集(train dataset)中的6000张图像和测试数据集(test dataset)中的1000张图像组成。因此,训练集和测试集分别包含60000和10000张图像。
3.5.1 读取数据集
#导入相关库
%matplotlib inline
import torch
import torchvision
from torch.utils import data
from torchvision import transforms
from d2l import torch as d2l
d2l.use_svg_display()
#通过框架内置函数下载读取数据集
# 通过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)
结果:
查询数据集图像的尺寸像素(通道数,高度h像素、宽度w像素):
mnist_train[0][0].shape
结果:显示结果为灰度图像,1个通道,高、宽像素均为28.
Fashion‐MNIST中包含的10个类别,分别为t‐shirt(T恤)、trouser(裤子)、pullover(套衫)、dress(连衣裙)、coat(外套)、sandal(凉鞋)、shirt(衬衫)、sneaker(运动鞋)、bag(包)和ankle boot(短靴)。以下函数用于在数字标签索引及其文本名称之间进行转换。
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]
可视化样本:
def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): #@save
"""绘制图像列表"""
figsize = (num_cols * scale, num_rows * scale)
_, axes = d2l.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));
结果:
3.5.2 读取小批量
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=True表示打乱顺序
读取训练数据所需的时间:
timer = d2l.Timer()
for X, y in train_iter:
continue
f'{timer.stop():.2f} sec'
结果:
3.5.3 整合所有组件
定义load_data_fashion_mnist函数,用于获取和读取Fashion‐MNIST数据集。这个函数返回训练集和验证集的数据迭代器。此外,这个函数还接受一个可选参数resize,用来将图像大小调整为另一种形状。
def load_data_fashion_mnist(batch_size, resize=None): #@save
"""下载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()))
通过指定resize参数来测试load_data_fashion_mnist函数的图像大小调整功能:
train_iter, test_iter = load_data_fashion_mnist(32, resize=64)
for X, y in train_iter:
print(X.shape, X.dtype, y.shape, y.dtype)
break
结果:
3.6 softmax回归的从零开始实现
后续再补充