优化神经网络

#主要名词解释
<details><summary>名词一览</summary><div>

张量Tensor

后向传播
前向传播

均方误差函数
梯度下降法
链式求导
自动求导->求导数的物理意义？？

线性变换、
激活函数、
线性激活函数、
非线性激活函数

卷积层、
全连接层、
池化层

过拟合

后向传播中的链式求导

批量 即Batch，是深度学习中的一个重要概念

在线学习和离线学习
递增算法
偏移/阈值 Bias
对于线性输出神经元，偏移项就是回归中的截距项

标准化数据：
重放缩（Rescaling）
规范化（Normalization）
标准化（Standardization）

torch.rand(x,y)
随即生成浮点数据在0-1区间的均匀分布

torch.randn(x,y)
随机生成的浮点数的取值满足均值为0，方差为1的正态分布

</div></details>

#初始化训练变量

```python

import torch

#batch_n是在一个批次中输入数据的数量，值是100，这意味着我们在一个批次中输入100个数据 
batch_n = 100

#同时，每个数据包含的数据特征有input_data个，
#因为input_data的值是1000，所以每个数据的特征就是1000个，
#也就是一次输入是一个100行1000列的矩阵数组[100,1000]
input_data = 1000

#hidden_layer用于定义经过隐藏层后保留的数据特征的个数，这里有100个，
#因为我们的模型只考虑一层隐藏层，所以在代码中仅仅定义了一个隐藏层的参数；
#经过隐藏层过滤后，输出数据为[100,100]
hidden_layer = 100

#output_data是输出的数据，值是10，我们可以将输出的数据看作一个分类结果值得数量，
#个数10表示我们最后要得到10个分类结果值。
#最终的输出数据为[100,10]
output_data = 10

#一次模型的训练
#在得到输出结果之后计算损失并进行后向传播，
#这样一次模型的训练就完成了，
#然后训练这个流程就可以完成指定次数的训练，并达到优化模型参数的目的。
[100,1000] -> [100,100] -> [100,10]
```

#初始化权重

```python

#[100,1000]输入层纬度
x = torch.randn(batch_n,input_data)

#[100,10]输出层纬度
y = torch.randn(batch_n,output_data)

#随即权重参数初始化（尽管这并不是一个好主意）
#[1000,100]输入层到隐藏层对应的权重参数
w1 = torch.randn(input_data,hidden_layer)
#[100,10]隐藏层到输出层对应的权重参数
w2 = torch.randn(hidden_layer,output_data)

#这里我们可能会好奇权重参数的维度是如何定义下来的，
#其实，只要我们把整个过程看作矩阵连续的乘法运算，就自然能够明白了，
x * w1 * w2 = y
[100,1000] * [1000,100] * [100,10] = [100,10]
#在代码中我们的真实值y也是通过随机的方式生成的，
#所以一开始在使用损失函数计算损失值时得到的结果会较大。
```

#定义训练次数和学习效率

```python

#epoch_n的值为20，所以我们需要通过循环的方式让程序进行20次训练
epoch_n = 20

#在优化的过程中使用的学习效率learning_rate的值为1e-6，表示0.000001
learning_rate = 1e-6

#使用梯度下降的方法来优化神经网络的参数
for epoch in range(epoch_n):

    #前向传播得到的预测结果通过 y_pred来表示
    #在这个神经网络的前向传播中，通过两个连续的矩阵乘法计算出预测结果，
    #在计算的过程中还对矩阵乘积的结果使用clamp方法进行裁剪，
    #将小于零的值全部重新赋值于0，这就像加上了一个ReLU激活函数的功能。
    h1 = x.mm(w1)  # 100*1000
    h1 = h1.clamp(min=0)
    y_pred = h1.mm(w2)  # 100*10
    # print(y_pred)

    #简化写法
    #y_pred = x.mm(w1).clamp(min=0).mm(w2)
 
    #在得到了预测值后就可以使用预测值和真实值来计算误差值了。
    #我们用loss来表示误差值，对误差值的计算使用了均方误差函数。
    loss = (y_pred - y).pow(2).sum()
    print("Epoch:{} , Loss:{:.4f}".format(epoch, loss))
 
    #我们的代码实现使用的是每个节点的链式求导结果，
    #在通过计算之后，就能够得到每个权重参数对应的梯度分别是grad_w1和grad_w2
    gray_y_pred = 2 * (y_pred - y)
    gray_w2 = h1.t().mm(gray_y_pred)
 
    grad_h = gray_y_pred.clone()
    grad_h = grad_h.mm(w2.t())
    grad_h.clamp_(min=0)
    grad_w1 = x.t().mm(grad_h)
 
    #在得到参数的梯度值之后，按照之前定义好的学习速率对w1和w2的权重参数进行更新
    w1 -= learning_rate * grad_w1
    w2 -= learning_rate * gray_w2
```

# 自定义一个模型model
```python

class Model(torch.nn.Module):
    def __init__(self):
        super(Model,self).__init__()
     
    def forward(self,input,w1,w2):
        x = torch.mm(input,w1)
        x = torch.clamp(x,min=0)
        x = torch.mm(x,w2)
        return x
     
    def backward(self):
        pass

model = Model()
for epoch in range(epoch_n):
 
    # y_pred = x.mm(w1).clamp(min= 0 ).mm(w2)
    y_pred = model(x, w1, w2)
    loss = (y_pred - y).pow(2).sum()
    print("Epoch:{} , Loss:{:.4f}".format(epoch, loss.data[0]))
 
    #自动梯度
    loss.backward()
    w1.data -= learning_rate * w1.grad.data
    w2.data -= learning_rate * w2.grad.data
 
    w1.grad.data.zero_()
    w2.grad.data.zero_()

```

#定义神经网络模型
```python

#使用torch.nn中的线性层加激活函数配合torch.optim完成神经网络模型的搭建和模型参数的优化
#使用cnn类来自动生成和初始化对应维度的权重参数 w1,w2就可以不用随机生成了

models = torch.nn.Sequential(OrderedDict([
    ("Linel",torch.nn.Linear(input_data,hidden_layer)),
    ("ReLU1",torch.nn.ReLU()),
    ("Line2",torch.nn.Linear(hidden_layer,output_data))
])
)
#torch.nn包中已经定义好的均方误差函数类torch.nn.MSELoss来计算损失值
loss_fn = torch.nn.MSELoss()

#训练代码
for epoch in range(epoch_n):
    y_pred = models(x)
    loss = loss_fn(y_pred,y)
    if epoch%1000 == 0:
        print("Epoch:{},Loss:{:.4f}".format(epoch,loss.data[0]))
    models.zero_grad()
 
    loss.backward()
 
    for param in models.parameters():
        param.data -= param.grad.data*learning_rate

```

#优化模型

```python

#在PyTorch的torch.optim包中提供了非常多的可实现参数自动优化的类，
#比如SGD、AdaGrad、RMSProp、Adam等，这些类都可以被直接调用，使用起来也非常方便。
optimzer = torch.optim.Adam(models.parameters(),lr = learning_rate)

#进行模型训练
for epoch in range(epoch_n):
    y_pred = models(x)
    loss = loss_fn(y_pred,y)
    print("Epoch:{}, Loss:{:.4f}".format(epoch, loss.data[0]))
    optimzer.zero_grad()
 
    loss.backward()
 
    #进行梯度更新
    optimzer.step()

```
#卷积神经网络模型搭建的代码

```python

#模型搭建和参数优化
#在顺利完成数据装载后，我们可以开始编写卷积神经网络模型的搭建和参数优化的代码
#卷积层使用torch.nn.Conv2d类来搭建
#激活层使用torch.nn.ReLU 类方法来搭建
#池化层使用torch.nn.MaxPool2d类方法来搭建
#全连接层使用 torch.nn.Linear 类方法来搭建

#在结构上使用了两个卷积层 Conv2d
#一个最大池化层 MaxPool2d
#两个全连接层 Linear
class Model(torch.nn.Module):
    def __init__(self):
        super(Model,self).__init__()
        self.conv1 = torch.nn.Sequential(
            torch.nn.Conv2d(1,64,kernel_size=3,stride=1,padding=1),
            torch.nn.ReLU(),
            torch.nn.Conv2d(64,128,kernel_size=3,stride=1,padding=1),
            torch.nn.ReLU(),
            torch.nn.MaxPool2d(stride=2,kernel_size=2))
 
        self.dense = torch.nn.Sequential(# 我们通过继承torch.nn.Modeule来构造网络，因为手写数字
    # 识别比较简单，我们只是用了两个卷积层，一个最大池化层，两个全连接层。
    # 在向前传播过程中进行x.view(-1, 14 * 14 * 128)
    # 对参数实现扁平化。最后通过自己self.dense定义的全连接层进行最后的分类
            torch.nn.Linear(14*14*128,1024),
            torch.nn.ReLU(),
            torch.nn.Dropout(p = 0.5),
            torch.nn.Linear(1024,10)
        )
 
    # 我们通过继承torch.nn.Modeule来构造网络，因为手写数字
    # 识别比较简单，我们只是用了两个卷积层，一个最大池化层，两个全连接层。
    # 在向前传播过程中进行x.view(-1, 14 * 14 * 128)
    # 对参数实现扁平化。最后通过自己self.dense定义的全连接层进行最后的分类
    def forward(self, x):
        #进行卷积处理
        x = self.conv1(x)
        #对参数实现扁平化
        #如果不进行扁平化，则全连接层的实际输出的参数维度和其定义输入的维度将不匹配，程序将会报错
        x = x.view(-1,14*14*128)
        #全连接层进行最后的分类
        x = self.dense(x)
        return x
```

# 全连接网络 VS 卷积网络
## 全连接神经网络之所以不太适合图像识别任务，主要有以下几个方面的问题：

- 参数数量太多 考虑一个输入1000*1000像素的图片(一百万像素，现在已经不能算大图了)，输入层有1000*1000=100万节点。假设第一个隐藏层有100个节点(这个数量并不多)，那么仅这一层就有(1000*1000+1)*100=1亿参数，这实在是太多了！我们看到图像只扩大一点，参数数量就会多很多，因此它的扩展性很差。
- 没有利用像素之间的位置信息 对于图像识别任务来说，每个像素和其周围像素的联系是比较紧密的，和离得很远的像素的联系可能就很小了。如果一个神经元和上一层所有神经元相连，那么就相当于对于一个像素来说，把图像的所有像素都等同看待，这不符合前面的假设。当我们完成每个连接权重的学习之后，最终可能会发现，有大量的权重，它们的值都是很小的(也就是这些连接其实无关紧要)。努力学习大量并不重要的权重，这样的学习必将是非常低效的。
- 网络层数限制 我们知道网络层数越多其表达能力越强，但是通过梯度下降方法训练深度全连接神经网络很困难，因为全连接神经网络的梯度很难传递超过3层。因此，我们不可能得到一个很深的全连接神经网络，也就限制了它的能力。

##那么，卷积神经网络又是怎样解决这个问题的呢？主要有三个思路：

- 局部连接 这个是最容易想到的，每个神经元不再和上一层的所有神经元相连，而只和一小部分神经元相连。这样就减少了很多参数。
- 权值共享 一组连接可以共享同一个权重，而不是每个连接有一个不同的权重，这样又减少了很多参数。
- 下采样 可以使用Pooling来减少每层的样本数，进一步减少参数数量，同时还可以提升模型的鲁棒性。
对于图像识别任务来说，卷积神经网络通过尽可能保留重要的参数，去掉大量不重要的参数，来达到更好的学习效果。