前言:
这个博客《d2l-ai深度学习日记》将记录我在深度学习领域的学习与探索,特别是基于《动手学深度学习》这本经典教材的学习过程。在这个过程中,我不仅希望总结所学,还希望通过分享心得,与志同道合的朋友一起交流成长。这不仅是对知识的沉淀,也是我备战研究生考试、追逐学术进阶之路的一部分。
一.线性回归初步了解
1.矢量化加速:
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)
c = torch.zeros(n)
timer = Timer()
for i in range(n):
c[i] = a[i] + b[i]
f'{timer.stop():.5f} sec'
timer.start()
d = a + b
f'{timer.stop():.5f} sec'
输出:
'0.33865 sec'
'0.00100 sec'
这段代码说明合理利用线性代数库,而不是粗暴地使用循环,可以极大地提高我们的效率,这里做了解即可.
2.画出正态分布图像
定义正态分布函数:
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)
画出三个正态分布函数并进行可视化
# 再次使⽤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])
二.线性回归从零开始实现
1.创建简单数据集
def synthetic_data(w, b, num_examples): #@save
# """⽣成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))
true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)
print('features:', features[0],'\nlabel:', labels[0])
d2l.set_figsize()
d2l.plt.scatter(features[:, (1)].detach().numpy(), labels.detach().numpy(), 1);
输出:
features: tensor([0.9625, 1.5589])
label: tensor([0.8252])
这里创建数据集原理为,首先定义直线函数:y=w*x+b,人为地去定义w和b作为直线的斜率和截距,然后通过正态分布取自变量x,即取点,再计算出相应的y,得到y之后再加上通过torch.nomal正态分布生成的噪点,就得到了上图所示的图案.
值得注意的是,这里的w和b是tensor张量,故实际上直线函数应该为
后面得到features
和 labels,features
是由函数 synthetic_data
生成的二维张量,表示输入的特征矩阵,即每一行表示一个样本的特征向量。
labels
是目标值(或称为标签),对应每个样本的真实输出。在这里,标签是通过线性方程 y = Xw + b + 噪声
计算得到的
具体来说,对于第一个样本,特征向量为 X = [0.9625, 1.5589]
,给定的权重向量 w = [2, -3.4]
和偏置 b = 4.2
,线性回归方程为:
y=Xw+b=(0.9625×2)+(1.5589×−3.4)+4.2
计算得到:
y=(0.9625×2)+(1.5589×−3.4)+4.2=1.925+(−5.301)+4.2=0.824
然后加入了一个微小的噪声值,所以得到的标签值为 0.8252
2.读取数据集
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]
这里使用小批量读取,random.shuffle(indices)的作用是
将样本的索引列表打乱,目的是在每次迭代时以不同的随机顺序读取数据,防止模型因数据顺序问题产生偏差。
atch_indices = torch.tensor(indices[i: min(i + batch_size, num_examples)])
这里通过 indices[i: min(i + batch_size, num_examples)]
获取当前批量的样本索引,确保最后一批次的数据不超出样本总数。将这些索引转换为 PyTorch 张量 batch_indices
,方便后续从 features
和 labels
中索引出对应的样本。
3.初始化模型参数
batch_size = 10
w = torch.normal(0, 0.01, size=(2,1), requires_grad=True)
# w = torch.zeros((2,1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
def linreg(X, w, b): #@save
# """线性回归模型"""
return torch.matmul(X, w) + b
def squared_loss(y_hat, y): #@save
# """均⽅损失"""
return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
def sgd(params, lr, batch_size): #@save
# """⼩批量随机梯度下降"""
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_()
lr = 0.03
num_epochs = 20
net = linreg
loss = squared_loss
4.训练
for epoch in range(num_epochs):
for X, y in data_iter(batch_size, features, labels):
l = loss(net(X, w, b), y) # X和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}')
print(f'w的估计误差: {true_w - w.reshape(true_w.shape)}')
print(f'b的估计误差: {true_b - b}')
输出:
epoch 1, loss 0.036724
epoch 2, loss 0.000150
epoch 3, loss 0.000053
epoch 4, loss 0.000053
epoch 5, loss 0.000053
epoch 6, loss 0.000053
epoch 7, loss 0.000053
epoch 8, loss 0.000053
epoch 9, loss 0.000053
epoch 10, loss 0.000053
epoch 11, loss 0.000053
epoch 12, loss 0.000053
epoch 13, loss 0.000053
epoch 14, loss 0.000053
epoch 15, loss 0.000053
epoch 16, loss 0.000053
epoch 17, loss 0.000053
epoch 18, loss 0.000053
epoch 19, loss 0.000053
epoch 20, loss 0.000053
w的估计误差: tensor([-0.0002, 0.0001], grad_fn=<SubBackward0>)
b的估计误差: tensor([0.0002], grad_fn=<RsubBackward1>)
我这里没有借助太多其他工具,而是自己详细定义了自己的损失函数,优化方法函数,并且由于是人工创造的函数,所以收敛得非常快,在epoch 3得时候误差就已经非常小了.这里对sgd函数详细解释一下:
sgd([w, b], lr, batch_size)
:这个函数用于更新模型的权重 w
和偏置 b
。
[w, b]
:是一个包含模型参数的列表,表示需要更新的模型参数,包括权重w
和偏置b
。lr
:是学习率,控制每次更新时参数的变化幅度。学习率越大,参数变化越快;学习率越小,参数变化越慢。batch_size
:表示当前批量数据的大小。由于梯度是对小批量数据求得的,需要将梯度按批量大小进行平均。
梯度更新的数学计算比较复杂,不需要掌握得太好.
三.线性回归的简洁实现
1.⽣成数据集
true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = d2l.synthetic_data(true_w, true_b, 1000)
2.读取数据集
def load_array(data_arrays, batch_size, is_train=True): #@save
# """构造⼀个PyTorch数据迭代器"""
dataset = data.TensorDataset(*data_arrays)
return data.DataLoader(dataset, batch_size, shuffle=is_train)
3.初始化参数
#人工制造数据
true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = d2l.synthetic_data(true_w, true_b, 1000)
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)
这里详细解释一下:
Sequential类相当于是一个网络的容器,里面可以填充很多层网络,虽然我们这里只有一层网络,但是由于以后⼏乎所有的模型都是多层的,所以书中在这⾥使⽤Sequential来让我们熟悉“标准的流⽔线”.
weight.data.normal_(0, 0.01)
:将权重初始化为均值为 0
,标准差为 0.01
的正态分布随机数。初始化权重可以帮助模型在训练时更有效地收敛。
bias.data.fill_(0)
:将偏置初始化为 0
。
torch.optim.SGD
:这是随机梯度下降(SGD)优化器,用于更新模型参数。它根据每一批数据的梯度,使用学习率 lr=0.03
来更新模型参数。
可以看到,这里设置的参数其实和我们自己详细实现线性回归网络训练几乎一样
4.训练
num_epochs = 5
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.000104
epoch 2, loss 0.000104
epoch 3, loss 0.000105
epoch 4, loss 0.000104
epoch 5, loss 0.000104
w的估计误差: tensor([4.1366e-04, 2.3842e-05])
b的估计误差: tensor([-0.0004])
可以看到,只用了几乎一个epoch就训练收敛了,说明线性回归模型确实很简单,能够很好地帮助我们来初步理解复杂的机器学习,总的来说,目前的机器训练分为以下几步:1.获取数据集;2.构造网络;3.构造优化方式,损失函数;4.设置初始参数:学习率,训练次数(即epoch);5.训练后得到数据
四.softmax回归图像分类数据集
在构造了最简单的线性回归网络之后,下一步开始学习简单的图像分类网络,因为相比于输出数字的网络,图像的计算机处理是更容易理解的.教材在这一章中给出了许多复杂的公式,我将会在下面构造神经网络的时候,详细说明为什么教材要给出这么多公式
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)
mnist_train[0][0].shape#测试是否下载成功
值得注意的是,这里root的值应该修改为自己数据集想要下载的位置
2.可视化数据集
先根据书中,构造一个数据集标签列表
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]
再使用plt绘制图像列表:
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));
输出:
大致了解一些数据集都是哪些图片
五.softmax回归的从零开始实现
数据集在上面已经下载了,就不多赘述
1.初始化模型参数
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)
看到上图以及get_fashion_mnist_labels函数,明显有10种图片,所以输出的数目就是10;然后每张图片的像素都是28×28,把其简化,转化为列向量,就是784,故输入是784.w和b就类似于线性回归里面的权重和偏置,分别初始化为正态分布和0.
2.定义softmax操作
softmax回归和线性回归的输出类似,都是输出一组数字,但是我们如何把这些数字转化为分类呢?在初始化模型参数代码中可以看到我们的输出是10,也就是说使用一个长度为10的向量表示,即使用独热编码,举例来说:
假设每次输⼊是⼀个2 × 2的灰度图像,每个图像属于类别“猫”,“鸡”和“狗”中的⼀个,独热编码是⼀个向量,它的分量和类别⼀样多。类别对 应的分量设置为1,其他所有分量设置为0。在我们的例⼦中,标签y将是⼀个三维向量,其中(1, 0, 0)对应于 “猫”、(0, 1, 0)对应于“鸡”、(0, 0, 1)对应于“狗”:
y ∈ {(1, 0, 0), (0, 1, 0), (0, 0, 1)}.
这样后,我们进一步将分量作为概率,譬如(0.2,0.6,0.2)就表示预测输入图像20%是猫,60%是鸡,20%是狗,由于输出的列向量可能是(2,6,2)或者(-1,3,-1)的值,虽然可以看出数值之间的大小关系得到预测种类,但是并不规划,softmax操作就是把这些列向量规范化,具体公式如下:
softmax操作代码:
def softmax(X):
X_exp = torch.exp(X)
partition = X_exp.sum(1, keepdim=True)
return X_exp / partition # 这⾥应⽤了⼴播机制
3.定义损失函数
我们引入概率交叉熵函数:
其中p(x)表示真实标签的概率,q(x)表示模型预测的概率,这里只对该公式进行了解,它是分类问题最常⽤的损失之⼀.
实现代码:
def cross_entropy(y_hat, y):
return - torch.log(y_hat[range(len(y_hat)), y])
4.计算分类精度
当预测与标签分类y⼀致时,即是正确的,分类精度即正确预测数量与总预测数量之⽐。
计算分类精度代码:
def accuracy(y_hat, y): #@save
# """计算预测正确的数量"""
if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
y_hat = y_hat.argmax(axis=1)
cmp = y_hat.type(y.dtype) == y
return float(cmp.type(y.dtype).sum())
下面,为了方便我们在训练的时候,得到模型的精度,构建两个函数:
累加器:
class Accumulator: #@save
# """在n个变量上累加"""
def __init__(self, n):
self.data = [0.0] * n
def add(self, *args):
self.data = [a + float(b) for a, b in zip(self.data, args)]
def reset(self):
self.data = [0.0] * len(self.data)
def __getitem__(self, idx):
return self.data[idx]
不用去纠结这个累加器,知道这是一个用于累加值的工具就行
计算在指定数据集上模型的精度迭代器:
def evaluate_accuracy(net, data_iter): #@save
# """计算在指定数据集上模型的精度"""
if isinstance(net, torch.nn.Module):
net.eval() # 将模型设置为评估模式
metric = Accumulator(2) # 正确预测数、预测总数
with torch.no_grad():
for X, y in data_iter:
metric.add(accuracy(net(X), y), y.numel())
return metric[0] / metric[1]
代码解释:
isinstance(net, torch.nn.Module)
:检查 net
是否为 PyTorch 的神经网络模型。确保该函数适用于 PyTorch 模型。
Accumulator(2)
初始化了两个计数器,一个用于存储正确预测的样本数,另一个用于存储总样本数。用来存储计算模型精度的数据.
metric.add(accuracy(net(X), y), y.numel())
:
net(X)
:对输入X
进行预测,得到模型的输出。accuracy(net(X), y)
:计算模型预测结果与真实标签y
的准确性。y.numel()
:返回标签y
中元素的数量,也就是当前批次的样本数量。metric.add
:将正确预测数和样本总数分别加到累加器中。
5.训练
训练代码:
定义训练函数:
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): #@save
# """训练模型(定义⻅第3章)"""
animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],legend=['train loss', 'train acc', 'test acc'])
for epoch in range(num_epochs):
train_metrics = train_epoch_ch3(net, train_iter, loss, updater)
test_acc = evaluate_accuracy(net, test_iter)
animator.add(epoch + 1, train_metrics + (test_acc,))
train_loss, train_acc = train_metrics
assert train_loss < 0.5, train_loss
assert train_acc <= 1 and train_acc > 0.7, train_acc
assert test_acc <= 1 and test_acc > 0.7, test_acc
这里看到有一个animator变量,它的作用是随着训练,画出模型精度和epoch的图像,具体代码如下:
class Animator: #@save
# """在动画中绘制数据"""
def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None,ylim=None, xscale='linear', yscale='linear', fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1, figsize=(3.5, 2.5)):
# 增量地绘制多条线
if legend is None:
legend = []
d2l.use_svg_display()
self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize)
if nrows * ncols == 1:
self.axes = [self.axes, ]
# 使⽤lambda函数捕获参数
self.config_axes = lambda: d2l.set_axes(self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
self.X, self.Y, self.fmts = None, None, fmts
def add(self, x, y):
# 向图表中添加多个数据点
if not hasattr(y, "__len__"):
y = [y]
n = len(y)
if not hasattr(x, "__len__"):
x = [x] * n
if not self.X:
self.X = [[] for _ in range(n)]
if not self.Y:
self.Y = [[] for _ in range(n)]
for i, (a, b) in enumerate(zip(x, y)):
if a is not None and b is not None:
self.X[i].append(a)
self.Y[i].append(b)
self.axes[0].cla()
for x, y, fmt in zip(self.X, self.Y, self.fmts):
self.axes[0].plot(x, y, fmt)
self.config_axes()
display.display(self.fig)
display.clear_output(wait=True)
这个函数比较复杂,了解一下就行,重点看train_ch3,里面的函数基本上都是我自己写的函数,容易理解
开始训练:
lr = 0.1
def updater(batch_size):
return d2l.sgd([W, b], lr, batch_size)
num_epochs = 10
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater)
这里定制一个更新器,还是使用和线性回归一样使用梯度下降法进行训练.下面看到一系列训练变化图像
train loss是损失函数值,明显呈下降趋势, train acc和test acc分别是模型在训练数据集和测试数据集里面的精度,明显显著地提高,可以看到,我训练地模型不仅在用于训练地数据集中效果良好,在广义上,模型未见过地测试集中也效果不错.
6.预测
def predict_ch3(net, test_iter, n=6): #@save
# """预测标签(定义⻅第3章)"""
for X, y in test_iter:
break
trues = d2l.get_fashion_mnist_labels(y)
preds = d2l.get_fashion_mnist_labels(net(X).argmax(axis=1))
titles = [true +'\n' + pred for true, pred in zip(trues, preds)]
d2l.show_images(X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n])
predict_ch3(net, test_iter)
结果:
可见正确率非常高
六.softmax回归的简洁实现
在已经大致理解了softmax回归的大概原理之后,为了方便操作,直接通过深度学习框架的⾼级API实现.
1.获取数据集
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
2.初始化模型参数
# 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);
直接使用Sequential函数来定义网络层,这里的
这里的 nn.Linear(784, 10)
是一个线性层,同样表示输入有784个,输出个10个数.
3.损失函数和优化函数
loss = nn.CrossEntropyLoss(reduction='none')
trainer = torch.optim.SGD(net.parameters(), lr=0.1)
直接调用高级api,两行解决交叉熵损失函数和梯度下降优化器
4.训练
num_epochs = 10
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
值得注意的是,d2l库在我使用的版本1.0.3已经移除了函数train_ch3,所以我会报错
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[19], line 2
1 num_epochs = 10
----> 2 d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
AttributeError: module 'd2l.torch' has no attribute 'train_ch3'
需要把python下降版本,并下载0.17.6的d2l库,读者可自行尝试,我没有重新下载d2l库,可以使用
pip show d2l
来查看自己安装的d2l版本
七.总结
初步了解,并自己编写了自己的网络进行训练,非常有成就感,发现神经网络也没有自己想象中那么复杂,唯一的难点就是数学公式太过于复杂,不过只要了解其中的大概原理,问题也都是迎刃而解.学习了线性回归网络和softmax回归图像分类网络两个单层网络,并且在明白网络的原理上面,可以使用高级api,使用简洁的代码编写自己的网络.
标签:loss,num,ai,self,epoch,train,d2l,net,日记 From: https://blog.csdn.net/Wyh666a/article/details/142440848