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

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

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

基础知识

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

  • 在全连接之后,会有一个激活函数处理输入值,即 σ ( f ( z = W T x + b ) ) \sigma(f(z=W^Tx+b)) σ(f(z=WTx+b)),然后每个神经元又会把这个活性值传递给下一层
  • 如何使用交叉熵获取模型损失? 交叉熵内容可以参考我的之前的博客内容。
数据集

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

代码

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

'''
使用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)
            loss.backward()
            opti.step()
            opti.zero_grad()
            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]}")
分析

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

  • loss.backward() 有什么作用?
    如果去掉这行代码,为啥模型就不能优化了(模型参数就不再变化)?那opti 的作用又是什么?
  • Normalize的归一化操作具体是怎么执行的?