从网上找到了一道面试题,花了我大半天的时间才写出来,还是太嫩了,不能熟练的给出代码,像下面这种代码我觉得最多20min吧。作为一名算法工程师,pytorch的API接口都应该十分熟悉才行!接下来的任务包括背pytorch的接口!


真实面试题,请实现下面这个需求


对MNIST 手写数据集进行分类。神经网络需具备如下三个要求:(1)使用三层全连接;(2)具有非线性变化;(3)具有过拟合处理手法


1. 基础知识

本文做的是使用前馈神经网络(DNN/FNN)来实现手写数字识别任务。需要注意如下几点:


  • 在全连接之后,会有一个激活函数处理输入值,即 ,然后每个神经元又会把这个活性值传递给下一层
  • 如何使用交叉熵获取模型损失? 交叉熵内容可以参考我的之前的博客内容。

2. 数据集

使用的数据集是MNIST 手写数字集,我们可以直接在​​torchvision​​中获取,具体方法可以参考我下面的代码。

3. 代码

我会针对代码逐行分析,然后给出可以优化的点(比如可视化损失下降;归一化处理输入等),查看有哪些东西需要注意。

'''
使用3层前馈神经网络完成图片分类训练。
'''
# from torch.utils.tensorboard import SummaryWriter
from visdom import Visdom
from tqdm import tqdm
import torchvision
from torchvision import transforms
import torch as t
from torch.utils.data import DataLoader,Dataset
import torch.nn as nn
from torchvision.datasets import mnist

transform = transforms.Compose(
[transforms.ToTensor(), # 函数接受PIL Image或numpy.ndarray,将其先由HWC转置为CHW格式,再转为float后每个像素除以255.
transforms.Normalize((0.1,), (0.34,)) # 这个方法必须放在ToTensor()之后
])


class Model(nn.Module):
def __init__(self,in_1,out_1,in_2,out_2,in_3,out_3):
super().__init__() # 因为继承了nn.Module类,所以这里必须要先调用一下父类。即初始化父类的属性
# 定义三个线性层
self.linear_1 = nn.Linear(in_features= in_1, out_features= out_1)
self.linear_2 = nn.Linear(in_features= in_2, out_features= out_2)
self.linear_3 = nn.Linear(in_features= in_3, out_features= out_3)

# 使用激活函数,后面测试使用relu 和 sigmoid 激活对模型的区别
# sigmoid 函数可以用多次吗? => 可以的
# self.active = nn.Sigmoid()
self.active = nn.ReLU()

# 防止过拟合
self.dropout = nn.Dropout()

# 输入数据是x
def forward(self,x):
# 有没有简单的不用使用堆叠的方法?
tempx = self.linear_1(x)
tempx = self.active(tempx)
tempx = self.linear_2(tempx)
tempx = self.active(tempx)
tempx = self.linear_3(tempx)

return tempx

if __name__ == "__main__":
#加载数据
# 这个raw_mnist_data 是一个 PIL.Image.Image 类型的数据,需要转换成tensor,所以这里用了一个转换操作
# 得到的数据类型是 torchvision.datasets.MNIST 。其中的数据作为其属性存储
mnist_data = torchvision.datasets.MNIST('./data/',download=True,train=True,transform=transform)
train_data = mnist_data.train_data.float()
#tran = transforms.Normalize((0),(255))
#train_data = tran(train_data)
print(train_data[0])
test_data = mnist_data.test_data.float()
train_labels = mnist_data.train_labels
test_labels = mnist_data.test_labels

# 对数据做处理
batch_size = 16
# 训练和测试数据
train_data_loader = DataLoader(train_data,
batch_size=batch_size,
# shuffle=True
)
train_label_loader = DataLoader(train_labels,
batch_size=batch_size,
# shuffle=True
)
test_data_loader = DataLoader(test_data,
batch_size=batch_size,
# shuffle=True
)
test_label_loader = DataLoader(test_labels,
batch_size=batch_size,
# shuffle=True
)
model = Model(784,256,256,128,128,10) #新建一个模型
train_epoch = 100
lr = 1e-3
opti = t.optim.Adam(model.parameters(),lr=lr) # 选择优化器
criterion = nn.CrossEntropyLoss() # 使用交叉熵作为损失

viz = Visdom()
win = "train_loss"
global_step = 0
model.train()
for epoch in tqdm(range(train_epoch)):
for data,gold in tqdm(zip(train_data_loader,train_label_loader)):
data = data.reshape(batch_size,-1) # 将28*28修改成一维向量
# print(data) # data.shape (batch_size,28,28) MNIST 中的数据集格式是28*28
# print(gold)
pred = model(data) # 得到返回的标签值
loss = criterion(pred,gold)
opti.zero_grad()
loss.backward()
opti.step()
global_step+=1
viz.line([loss.item()], [global_step],win=win, update="append")

# 打印优化参数的过程 => 验证是否优化了
for param in model.named_parameters():
name,value = param
if "linear_3.weight" in name:
print(value.shape)
print(name)
print(f"value={value[0,0:10]}")

4. 分析

平常写代码,都是能运行就可以了,也不管到底是几个意思,如果需要功能的时候直接从网上​​ctrl+c/v​​,这样子肯定是不行的。有下面几个问题值得考虑:

  • ​loss.backward()​​​ 有什么作用?
    如果去掉这行代码,为啥模型就不能优化了(模型参数就不再变化)?那​​​opti​​ 的作用又是什么?现在才思考这个问题确实显得有点儿外行了。
opti.zero_grad() # 优化器将之前计算得到的梯度值清零

# 开始反向传播,也就是通过loss 计算出对于输入值的梯度。优化A,就得执行A.backward()
loss.backward()
opti.step() # 使用上面计算好的梯度外加定义的优化器执行参数优化
  • ​Normalize​​的归一化操作具体是怎么执行的?