DL算法·俯瞰

 
算法选择
- 机器学习
- DL深度学习
  - CV 
  - NLP 

选择一个算法是为了解决一类问题,而DL算法通常包括以下三部分
或者说,将解决某个问题的算法,拆分为三部分
- 模型:网络参数结构定义
- 损失函数:衡量模型输出与数据标签的分布差异,值越小越好
- 优化器:调整模型的参数的方法 

模型只是定义了参数/参数结构 
怎么调整,尤其是如何自动化地调整模型的参数,
使得模型的输出与数据标签的分布越来越接近才是关键

损失函数与优化器配合,完成了参数的自动化调整
- 梯度下降法,偏导的正负就是下降的方向
- 没错,正负这个方向,比数值本身更重要
- 它为每个参数指明了下降的方向 
- w = w - a*w.grad 
  - a是一个小数,小于1,
  - w.grad是“损失函数”在参数w处的偏导
  - 损失函数关于某个参数的偏导函数,如果有极小值
  - 那么 w = w - a*w.grad 会慢慢使损失函数逼近这个极小值 
  - w.grad为正,w减小,小到一定程度,
  - 比如,在函数曲线上越过了极小值点,变大了,那么w.grad会变为负值
  - 此时的w = w - a*w.grad就会变大 
  - 损失函数取得极小值时的位置,w就在这个位置附近振荡
  - 极小值,是最低位置,w.grad为0,w并不一定为0 
  - 损失函数极小值,也不一定只有一个,可能会有多个
  - 极小,是相对一定范围的w来说的,可能其他范围也有极小
  - 就比如水坑,湖泊,河流,都有一个低极点,其偏导为0,但极点则有很多

 
从整个DL算法的解决来看,损失函数是入口,
输入的是模型与标签,
输出的是分布差异,
优化器是辅助工具,它负责更新损失函数中模型的参数
    

 
损失函数的结果是一个数值,随着训练,它的值可能是1000,800,500,300,100,80,... 
下降是我们期望的,即逐渐降低,变小,

自变量x数轴,右为负无穷,左为正无穷,从右到左,x逐渐变大 
即随着x的变大,损失函数变小
损失函数值降低的过程,即对应x处的导数为负数,
损失函数值上升的过程,即对应x处的导数为正数,

x = x - a*x.grad 会向一个极小值附近靠拢 
当导数为正,即x越过极小值时,x会变小,还会折回来,
然后就是在极小值附近来回移动... 

但这个极小值不一定就是0值,
即模型所能表述的规律,很难与真实的数据标签的分布 完全相符 
最好的结果就是:表象在规律附近振荡...

推导·简易版

数据加载,预处理

 
import torch
import os
from tpf import stp
from tpf import pkl_save,pkl_load 
from tpf.params import TPF_DATADIR

def load_boston(split=True,test_size=0.15, reload=False):
    """房价(回归问题),后续默认加载首次生成的文件 
    X,y = load_boston(split=False)
    X_train, y_train, X_test, y_test = load_boston(split=True,test_size=0.15) 

    print(type(X))   # class 'numpy.ndarray'
    print(X.shape)   # (506, 13)
    print(y.shape)   # (506,)
    """
    if split and (not reload):
        tmp_path = os.path.join(TPF_DATADIR,"fangjia_boston_split.pkl")
    else:
        tmp_path = os.path.join(TPF_DATADIR,"fangjia_boston.pkl")
    
    if os.path.exists(tmp_path):
        return pkl_load(tmp_path)

    data_url = "http://lib.stat.cmu.edu/datasets/boston"
    raw_df = pd.read_csv(data_url, sep="\s+", skiprows=22, header=None)
    # print(raw_df.info())


    data = np.hstack([raw_df.values[::2, :], raw_df.values[1::2, :2]])
    target = raw_df.values[1::2, 2]
    if split:
        X_train, X_test, y_train, y_test = train_test_split(data, target, test_size=test_size, random_state=73)
        stp(X_train,"X_train")
        stp(y_train,"y_train")
        pkl_save((X_train,y_train, X_test, y_test),file_path=tmp_path)
        return X_train,y_train, X_test, y_test
    else:
        stp(data, "X")
        stp(target,"y")
        pkl_save((data,target),file_path=tmp_path)
        return data,target


X_train,y_train, X_test, y_test = load_boston()
stp(X_test)  #shape:(76, 13), type:class 'numpy.ndarray'

# -------------------------------------------------
# 数据预处理
# -------------------------------------------------

def get_data():
    X_train,y_train, X_test, y_test = load_boston()

    # 从训练集中提取参数
    mean_ = X_train.mean(axis=0)   # 按列方向取均值,
    std_ = X_train.std(axis=0)     # 相同特征的一列数据归一

    # 预处理训练集和测试集
    X_train1 = (X_train - mean_) / std_
    X_test1 = (X_test - mean_) / std_ 

    # 共享内存
    X_train = torch.from_numpy(X_train1).float()
    X_test = torch.from_numpy(X_test1).to(dtype=torch.float32)

    # 标签转列向量
    y_train = torch.from_numpy(y_train.reshape((-1, 1))).float()
    y_test = torch.from_numpy(y_test.reshape((-1, 1))).float()

    return X_train,y_train, X_test, y_test

X_train,y_train, X_test, y_test = get_data()

# from tpf import stp 
# stp(y_test,"y_test")  # shape:torch.Size([76, 1]), type:class 'torch.Tensor', y_test
# print(y_test.dtype)   # torch.float32

    

模型

 
# -------------------------------------------------
# 模型构造
# -------------------------------------------------
class LinearRegression(object):
    """模型构造:线性回归
    """
    loss = None

    def __init__(self, device=None,in_feature=13) -> None:
        if device:
            self.device = device
        else:
            # 若存在GPU则使用GPU
            self.device = "cuda:0" if torch.cuda.is_available() else "cpu"
        

        # 定义模型的参数
        self.w = torch.randn(in_feature, 1, requires_grad=True, dtype=torch.float32, device=device)
        self.b = torch.randn(1, requires_grad=True, dtype=torch.float32, device=device)
        # print(b)   # tensor([0.1403], requires_grad=True)
        # stp(b,"b")   # shape:torch.Size([1]), type:class 'torch.Tensor', b

    def forward(self, X):
        return X@self.w + self.b

    # -------------------------------------------------
    # 损失函数设计
    # -------------------------------------------------
    @classmethod
    def loss_fn(cls, y_pred, y_true):
        cls.loss = ((y_pred - y_true) ** 2).mean()
        return cls.loss

    def grad_reduce(self, learning_rate):
        """优化方法:梯度下降法
        一种优化方法,让模型输出不断接近真实标签的方法
        梯度指导数
        """
        # 使用导数优化更新参数
        self.w.data -= learning_rate * self.w.grad.data
        self.b.data -= learning_rate * self.b.grad.data 

        # 清空梯度,以防止不断累加
        self.w.grad.data.zero_()
        self.b.grad.data.zero_()

    def print_loss(self):
        print(self.loss.item())

    
    def train(self, X,y, learning_rate, loss_fn=None):
        """训练
        每训练一次,就优化一次参数
        """

            # 正向传播
        y_pred = self.forward(X=X)

        # print("y pred",y_pred[1],"y label",y_train[1])
        
        # 模型预测与真实标签间的差异
        if loss_fn:
            loss = loss_fn(y_pred, y)
        else:
            loss = self.loss_fn(y_pred=y_pred, y_true=y_train)
        
        # 反向传播,逆向求偏导grad
        loss.backward()
        model.grad_reduce(learning_rate=learning_rate)


    def predict(self, X):
        """
            模型预测
        """
        with torch.no_grad():
            y_pred = self.forward(X=X)
        return y_pred 


def batch_train(model, X, y, learning_rate=1e-2, epochs=300):
    """训练
    梯度下降法求函数极值
    """
    for epoch in range(epochs):
        model.train(X=X,y=y, learning_rate=learning_rate)
        
        # 过程监控
        model.print_loss()
    
    

训练

 
# 若存在GPU则使用GPU
device = "cuda:0" if torch.cuda.is_available() else "cpu"
X_train.to(device=device)
y_train.to(device=device) 
X_test.to(device=device) 
y_test.to(device=device)

model = LinearRegression()
batch_train(model=model,X=X_train,y=y_train)
    

预测

 
y_pred = model.predict(X=X_test)
print(y_pred[:5])
    

损失

 
model.print_loss()
23.729873657226562
    

 
数据处理,正向传播,损失函数,优化器,训练,预测...

这些步骤中,理解难度大在的地方在于两点:正向传播, 优化器 

正向传播

 
def forward(self, X):
    return X@self.w + self.b
    
这涉及线性代数,也有微分的思想,
大概意思就是,众人拾柴火焰高,人多力量大,勤能补拙... 
以众多局部的直,去模拟实际的曲,

提一句微分,
把一件复杂的事,拆分成多个简单的成分,这就是微分,
从多个简单的角度去描述一件复杂的事物,这就是积分!
就是这么简单...只是有人把这种思想以数学的形式,精准地描述出来了!


难理解就是难理解,实现中,不只是普通人,
神经网络刚出来的时候,许多的学者,教授都不理解,不看好,认为不可能... 
本人在学到这个时,连连惊奇并研究了好几个月... 没错,本人并不聪明,一个简单的神经网络研究了好几个月!!!

 
X是常量,虽然每次可以输入不同的X,即不同的样本,但它还是常量
说它是常量,是因为X在整个计算过程中不变,就是一个常量“矩阵”

X是矩阵,是矩阵,是矩阵,w也是矩阵,也是矩阵,也是矩阵

 
但w是变量,是变化的,
对于不同的X,w是相同的,代表了数据集的共性提取方法 

任务的目的,就在于,对于某个数据集,
找到一个参数矩阵/参数网络,
它能够将常量X(即样本),映射到标签的维度上
    

优化器

 
def grad_reduce(self, learning_rate):
    """优化方法:梯度下降法
    一种优化方法,让模型输出不断接近真实标签的方法
    梯度指导数
    """
    # 使用导数优化更新参数
    self.w.data -= learning_rate * self.w.grad.data
    self.b.data -= learning_rate * self.b.grad.data 

    # 清空梯度,以防止不断累加
    self.w.grad.data.zero_()
    self.b.grad.data.zero_()
    
如果说前面的Ax=b是复杂问题简单化/线性化,是简单粗暴的微分描述,

那么
w = w - learning_rate*w.grad 
则是用规则/公式 去直接描述变化/变化趋势

也有微分的思想,因为这里的w指的是众多参数的中每一个 

这个公式的关键在于,
通过“损失函数”梯度的正负特性,建立参数/变量w与函数极小值之间的关系 

效果是
我们想找到模型输出与标签分布之间的最小差值,即损失函数的极小值,
那么可以通过公式w = w - learning_rate*w.grad 来调整参数w实现

 
w = w - learning_rate*w.grad

这公式赋予了神经网络自动化的能力,给予了神经网络超越机器学习的底气,
在深度学习发展,具有里程碑的意义!
    

 

    

 


 

  

 


参考
    反向传播算法推导过程(看一篇就够了)