搭建深度学习框架(一) 线性层,激活函数,损失函数

回顾

前面我们学习(复习)了线代,概率论和优化等数学课, 还复习了基础的机器学习算法, 逻辑回归 SVM和决策树等. 现在我们进入深度学习的第一课, 就是前馈神经网络的原理和使用. 神经网络是一种非常强大的机器学习模型, 在今天已经隐隐有取代其他所有传统模型的趋势. 我们之前使用统计概率模型来做NLP和语音识别, 但今天大多数从业者都会选择RNN; 在传统CV领域我们使用数字图像处理方法来做图像分割, 目标识别等任务, 但今天我们有R-CNN和其他更高级的神经网络架构能把这个事情做的更好. 我们甚至可以用Seq2Seq模型做神经机器翻译, 用GAN做图像风格转换和图像修复. 这一切都是那么激动人心.

线性层

在这里插入图片描述
我们前面已经讲到了逻辑回归, 它是非常简单的线性模型, 我们用m维向量x各个维度的线性组合y=xw+by = xw+b生成标量y, 作为二分类的判据. 前馈神经网络的线性层做的事情就是, 我们现在用输入x经过n个不同的w和b, 生成n个y, 一字排开变成新的n维向量. 这个过程就不是行向量xx和列向量ww相乘了, 而是m维行向量xx和m行n列的矩阵WW相乘, 再与一个n维的列向量b相加. 这时输出是n维的列向量y. 这就是线性层, 也就是pytorch的nn中实现的Linear模块.
forward ruley=xW+b forward\ rule\qquad y = xW+b
这个矩阵运算过程就是前向传播, 我们后面会把多个这样的线性层级联起来, 形成神经网络. 为了在这样的网络中计算每一个参数的梯度, 我们在优化时不仅要计算W和b的梯度, 还要计算x的梯度. 用矩阵乘法的求导法则和链式法则, 我们可以计算目标损失函数关于W,b和x的导数
W backward ruleLW=xTLy W\ backward\ rule\qquad \frac{\partial L}{\partial W} = x^T\frac{\partial L}{\partial y}
b backward ruleLb=SUMROWLy b\ backward\ rule\qquad \frac{\partial L}{\partial b} = SUMROW \frac{\partial L}{\partial y}
x backward ruleLx=LyWT x\ backward\ rule\qquad \frac{\partial L}{\partial x} = \frac{\partial L}{\partial y} W^T

激活函数

只有线性层的网络, 其本质就是多个矩阵相乘, 化简后得到的仍然是矩阵, 无法得到非线性. 为了让网络有非线性的能力, 我们会在线性层之前穿插激活函数.也就是整个神经网络的输出写做
o=(...acti((acti(xW1+b1))W2+b2)...)Wn+bn o = (...acti((acti(xW_1+b_1))W_2+b_2)...)W_n+b_n
激活函数接收m维向量, 在向量的每个元素上做非线性变换. 输出m维向量. 逻辑回归时我们用到了sigmoid这种函数, 它可以作为一种激活函数, 但它不是最好的函数. sigmoid虽然求导简单但是存在一些问题, 因为它的前向传播和反向传播计算导数的公式是
f(v)=11+e(v) f(v)=\frac {1} {1+e^{(-v)}}
f(v)=f(v)(1f(v)) f^{'}(v)=f(v)(1-f(v))
可以看见, 部分导是恒小于1的. 如果用这种激活函数就会在较为深层的网络(比如ResNet会有十几层的卷积层和线性层)中出现梯度的快速衰减, 以至于上层的线性层参数得不到有效更新, 进而无法有效训练网络. 在ResNet中我们会用一些技巧防止这个现象出现, 其中包括使用特殊的激活函数ReLU, 线性整流函数.
ReLU(v)=v if v>0 else 0 ReLU(v) = v\ if\ v>0\ else\ 0
ReLU(v)=1 if v>0 else 0 ReLU'(v) = 1\ if\ v>0\ else\ 0
在RNN中, 我们会用tanh这个函数代替sigmoid, 从而让梯度不衰减得太厉害.
tanh(v)=exexex+ex tanh(v) = \frac{e^x-e^{-x}}{e^x+e^{-x}}
tanh(v)=1(tanh(v))2 tanh'(v) = 1-(tanh(v))^2

Softmax

之前我们学习线性模型时,一般会构造多个判别器来实现多分类,但是神经网络有着更好的做法。我们知道神经网络允许多输出,那么能不能让神经网络直接输出每个类别的概率呢?答案是可以,为此我们要引入一个特殊的软化概率函数,以及BCEloss的原型,Cross Entropy Loss。
softmax是有限项离散概率分布的梯度对数归一化,也被称为归一化指数函数。softmax的计算如下σ(z)j=ezji=1Nezi \sigma(z)_j = \frac{e^{z_j}}{\sum_{i=1}^N e^{z_i}}
函数的输入是从K个不同的线性函数得到的结果,而样本向量 x 属于第 j 个分类的概率为softmax的输出。它相当于让神经网络输出了一个离散条件概率分布
p(z X,W) p(z\ |X,W)
即给定输入X和模型参数W下分类为z的概率,而我们在分类问题中希望优化的目标是:让训练集的真实条件概率分布和模型输出的概率分布尽可能相同。我们知道评估概率分布差异的方法是KL散度,而最优化KL散度的过程又可以简化为优化Cross Entropy的过程。离散概率分布的交叉熵写做
L(W)=1Ni=1Nlog(p(zi)) L(W) = -\frac{1}{N}\sum_{i=1}^{N}log(p(z_i))
它是模型接收第i个训练集后,我们预期的正确分类的概率的负对数的加和。我们假设训练集是从p(x)的真实采样,则此方法获得的是真实交叉熵的蒙特卡洛估计。把softmax的输出代入,可以计算单个样本输入时,L(W)的导数。
L(W)=log(ezji=1Nezi) L(W) = -log(\frac{e^{z_j}}{\sum_{i=1}^N e^{z_i}})
计算Loss关于各z_i输入的偏导数,要分为i=j和i!=j讨论
Lzi=ezik=1Nezk(ij) \frac{\partial{L}}{\partial{z_i}} = \frac{e^{z_i}}{\sum_{k=1}^N e^{z_k}} \quad \quad (i\neq j)
Lzi=ezik=1Nezkk=1Nezk=ezik=1Nezk1(i=j) \frac{\partial{L}}{\partial{z_i}} = \frac{e^{z_i}-\sum_{k=1}^N e^{z_k}}{\sum_{k=1}^N e^{z_k}}=\frac{e^{z_i}}{\sum_{k=1}^N e^{z_k}}-1 \quad \quad (i = j)
如果把标签写成one hot的向量,即正确标签为1,错误标签为0,就能给出非常简单的向量梯度形式
Lz=zlabels \frac{\partial{L}}{\partial{z}} = z-labels
我们实现一个softmax层,然后就可以搭建能完成多分类任务的神经网络了。

SGD with moentum

我们优化神经网络的一种有效方法是使用带动量的随机梯度下降,每次参数更新不是完全用当前的梯度,而是基于原本的下降方向作出调整。这样可以确定一个比较稳定的下降方向,本质上是共轭梯度法的简化版本。共轭梯度法希望通过方向修正,让所有的下降方向尽可能正交。而如果我们把修正系数设为常数,就是SGD with moentum算法。
dβd+(1β)αLW d \leftarrow \beta d+(1-\beta)\alpha\frac{\partial L}{\partial W}
如果不嫌麻烦, 也可以实现一下Adam优化算法, 这是一种比momentum更高效的算法.

Mni-Batch

使用批量梯度下降算法是数据量较大时一种比较快速的计算和训练方法, 我们上面神经网络的主要计算过程都是矩阵W和向量x的乘法运算, 如果我们把多个列向量x排成矩阵, 运算就变成矩阵乘矩阵. 矩阵和矩阵的计算比起矩阵和向量进行多次计算更为有效率. 在对拥有k个列向量的矩阵X前向传播计算梯度时, 我们对每个单独的向量x对应的dW和db求和, 就能一次性更新k步.

任务

  1. 实现反向传播算法,
  2. 不使用Pytorch的计算图模型反向传播. 实现梯度下降算法,
  3. 不使用torch.nn中任何辅助计算的激活函数和损失函数. 用numpy或pytorch实现Linear, ReLU和Softmax网络层, 以及CrossEntropyLoss损失函数. 使用这些原件搭建前馈神经网络, 并在MNIST数据集上测试, 要求达到90per以上的准确率.
  4. 除了backward和torch.nn以外, 可以自由使用其他任何来自torch和torchvision的辅助工具, 比如dataloader和矩阵乘法.
import torch
import numpy as np
import matplotlib.pyplot as plt

class ReLU:
    def __init__(self, slope = 0.1):
        self.mask = None
        self.leak = slope
        
    def forward(self, x):
        self.mask = x<0
        x[self.mask] *= self.leak
        return x
    
    def backward(self, dz):
        dz[self.mask] *= self.leak
        return dz
    
    def __call__(self, x):
        return self.forward(x)
    
class Sigmoid:
    def __init__(self):
        self.out = None
        
    def forward(self, x):
        self.out = 1/(1+torch.exp(-x))
        return self.out
    
    def backward(self, dz):
        return dz*self.out*(1-self.out)
    
    def __call__(self, x):
        return self.forward(x)
    
    
class Linear:
    def __init__(self,input_sz,output_sz, LEARNING_RATE=0.01,
                 momentum = 0.9):
        '''
        使用kaiming初始化策略, W = normal(0,2/(input_sz+output_sz))
        '''
        self.W = torch.randn(input_sz,output_sz)*(2/(input_sz+output_sz))
        self.b  = torch.randn(output_sz)*(2/output_sz)
        
        self.dW = torch.zeros_like(self.W)
        self.db = torch.zeros_like(self.b)
        
        self.lr = LEARNING_RATE
        self.momentum = momentum
        
        self.X = None
        
    def forward(self,X):
        self.X = X
        out =  torch.mm(self.X,self.W)+self.b
        return out
    
    def backward(self,dz):
        """
        dz-- 前面的导数
        基于反向传播的dz和动量、学习率,更新W和b
        """
        n,m = self.X.shape
        
        self.dW, self.db = self.dW*self.momentum, self.db*self.momentum
        dw = torch.mm(self.X.T,dz)#/n
        self.dW += self.lr*dw*(1-self.momentum)
        
        #db = torch.mean(dz, axis = 0)
        db = torch.sum(dz, axis = 0)
        self.db += self.lr*db*(1-self.momentum)
        
        dx = torch.mm(dz,self.W.T)
        
        return dx
    
    def update(self):
        #更新W和b
        self.W = self.W-self.dW
        self.b = self.b-self.db
        
    def zero_delta(self):
        # 清零dW和db
        self.dW = torch.zeros_like(self.W)
        self.db = torch.zeros_like(self.b)
    
    def __call__(self, X):
        return self.forward(X)


def cross_entropy_error(y_pred,labels):
    n,m = y_pred.shape
    return -torch.mean(torch.log(y_pred[range(n),labels]))
    
def softmax(X):
    n,m = X.shape
    exp_x = torch.exp(X)
    sum_exp_x = torch.sum(exp_x,axis=1)
    return (exp_x.T/sum_exp_x).T

def one_hot_encode(labels):
    '''
    labels:一维数组,返回独热编码后的二维数组
    '''
    n = len(labels)
    m = 10
    ret = torch.zeros((n,m))
    ret[range(n),labels] = 1.
    return ret


class SoftMax:
    '''
    softmax,归一化层,把输出转概率
    该层的输出代表每一分类的概率
    '''
    def __init__ (self):
        self.y_hat = None
        
    def forward(self,X):
        self.X = X
        self.y_hat = softmax(X)
        return self.y_hat
    
    def backward(self,labels):
        # 使用cross entropy loss
        dx = (self.y_hat-one_hot_encode(labels))
        return dx
    
    def __call__(self, x):
        return self.forward(x)
    

    
class Sequential:
    def __init__(self, module_list):
        self.layers = module_list
        
    def forward(self, x):
        for layer in self.layers:
            x = layer(x)
        return x
    
    def backward(self, dz):
        for layer in self.layers[::-1]:
            dz = layer.backward(dz)
        return dz
    
    def update(self):
        for layer in self.layers:
            if type(layer)==Linear:
                layer.update()
                
    def zero_delta(self):
        for layer in self.layers:
            if type(layer)==Linear:
                layer.zero_delta()
    
    def __call__(self, x):
        return self.forward(x)

算法收敛性测试:学习异或

使用sigmoid激活函数和线性层搭建最简单的神经网络, 解决逻辑回归分类器解决不了的异或问题.

xor_mechine = Sequential([Linear(2,2,0.1),Sigmoid(),Linear(2,1,0.1)])

input_v = torch.tensor([[0.,0.],[0.,1.],[1.,0.],[1.,1.]])

output_v = torch.tensor([[1.],[0.],[0.],[1.]])

for i in range(2000):
    y = xor_mechine(input_v)
    dz = y-output_v
    if i%100==0:
        print(torch.mean(dz**2))
    xor_mechine.backward(dz)
    xor_mechine.update()
    
xor_mechine(input_v)

tensor(0.8792)
tensor(0.2519)
tensor(0.2508)
tensor(0.2503)
tensor(0.2499)
tensor(0.2496)
tensor(0.2493)
tensor(0.2489)
tensor(0.2484)
tensor(0.2474)
tensor(0.2459)
tensor(0.2430)
tensor(0.2371)
tensor(0.2242)
tensor(0.1954)
tensor(0.1382)
tensor(0.0639)
tensor(0.0171)
tensor(0.0030)
tensor(0.0004)
tensor([[0.9908],
        [0.0082],
        [0.0080],
        [0.9953]])

全连接网络搭建与训练

class NN:    
    def __init__(self, LEARNING_RATE=0.01, MOMENTUM = 0.9):
        lr = LEARNING_RATE
        mom = MOMENTUM
        # 输入:28*28 = 784维特征
        self.nn = Sequential(
            [
            Linear(28*28,256,lr,mom),
            ReLU(),
            Linear(256,128,lr,mom),
            ReLU(),
            Linear(128,128,lr,mom),
            ReLU(),
            Linear(128,10,lr,mom),
            SoftMax()
            ]
        )
        # 输出:10维分类

    def forward(self, X):
        self.out = self.nn(X)
        return self.out

    def backward(self,y):
        # 使用softmax和交叉熵
        self.loss = cross_entropy_error(self.out,y)
        dz = self.nn.backward(y)
        return dz

    def update(self):
        self.nn.update()
        
    def zero_delta(self):
        self.nn.zero_delta()
        
    def __call__(self, x):
        return self.forward(x)

导入数据集

import torchvision
import torchvision.transforms as transforms

batch_size = 128

# MNIST dataset 
train_dataset = torchvision.datasets.MNIST(root='../mnist', 
                                           train=True, 
                                           transform=transforms.ToTensor())

test_dataset = torchvision.datasets.MNIST(root='../mnist', 
                                          train=False, 
                                          transform=transforms.ToTensor())

# Data loader
train_loader = torch.utils.data.DataLoader(dataset=train_dataset, 
                                           batch_size=batch_size, 
                                           shuffle=True)

test_loader = torch.utils.data.DataLoader(dataset=test_dataset, 
                                          batch_size=batch_size, 
                                          shuffle=False)

训练

num_epochs = 5
total_step = len(train_loader)
model = NN(LEARNING_RATE = 0.01)
loss_list = []

for epoch in range(num_epochs):
    running_loss = 0.
    for i, (images, labels) in enumerate(train_loader):
        images = images.reshape(-1, 28*28)
        # Forward pass
        outputs = model(images)
        
        # Backward and optimize
        model.backward(labels)
        model.update()
        
        running_loss += model.loss
        
        if (i+1) % 100 == 0:
            print ('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}' 
                   .format(epoch+1, num_epochs, i+1, total_step, running_loss.item()/100))
            loss_list.append(running_loss)
            running_loss = 0.

plt.plot(loss_list)
plt.title('Train loss(Cross Entropy)')

在这里插入图片描述

correct = 0
total = 0
for images, labels in test_loader:
    images = images.reshape(-1, 28*28)
    outputs = model(images)
    predicted = torch.argmax(outputs.data, axis = 1)
    total += labels.size(0)
    correct += (predicted == labels).sum().item()

print('Accuracy of the network on the 10000 test images: {} %'.format(100 * correct / total))

Accuracy of the network on the 10000 test images: 97.41 %

小结

本实验涉及深度学习的最基础知识, 包括线性层的设计, 损失函数的设计, 激活函数的设计和它们的部分偏导计算. 基于这些模块我们可以实现能处理较大型数据的分类器, 前馈神经网络.
后续的学习中我会介绍一些前馈神经网络的缺点, 以及用于弥补这些缺点的其他神经网络, 并同时实现它们.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章