一.深度学习是用来干什么的? 举个例子

假设我是上帝,我悄悄定了一个规则:

考试成绩 = 8.1 × 学习时间 + 2 × 作业完成度 + 2 × 课堂参与度 + 4 × 考前复习 + 1.1

然后我给了你一堆数据:

我只给你看这些,不告诉你规则
学生A: 学习时间=2h, 作业完成度=3, 参与度=4, 复习=1 → 考试成绩=8.1×2 + 2×3 + 2×4 + 4×1 + 1.1 = 16.2+6+8+4+1.1 = 35.3
学生B: 学习时间=1h, 作业完成度=5, 参与度=2, 复习=3 → 考试成绩=8.1×1 + 2×5 + 2×2 + 4×3 + 1.1 = 8.1+10+4+12+1.1 = 35.2
学生C: 学习时间=3h, 作业完成度=2, 参与度=5, 复习=2 → 考试成绩=8.1×3 + 2×2 + 2×5 + 4×2 + 1.1 = 24.3+4+10+8+1.1 = 47.4
......(500个学生) 

你的任务:只通过这些数据,反推出我的秘密规则(即找出 w₁=8.1, w₂=2, w₃=2, w₄=4, b=1.1)

你只是假装不知道w,b  我们通过深度学习的方式得到w b

深度学习就像一个超级侦探,通过不断试错来找出真相:

侦探的推理过程
第1次猜测:成绩 = 0.01×学习时间 + 0.01×作业完成度 + ...  # 完全不对,损失200+

通过猜测参数来跟真相对比发现有很大的差异就继续猜测
第2次猜测:稍微调整一下参数                          # 损失180+
...
第50次猜测:成绩 = 8.09×学习时间 + 2.01×作业完成度 + ... # 接近真相,损失0.5

二.代码实现

这个是深度学习的基本流程

数据准备 → 模型定义 → 损失函数 → 优化器 → 训练循环 → 评估可视化

接下来,使用代码带大家一步一步实现这个功能

首先引入我们需要的库

import torch   #PyTorch的核心,提供张量计算和自动求导
import matplotlib.pyplot as plt #画图工具,帮我们把数据可视化
import random #随机

然后我们需要生成一些模拟的数据,作为我们已知的数据(500个学生)

#我们先写一个函数来生成随机数据 这个函数需要接收
def create_data(w, b, data_num):
 # 从正态分布中随机取数 均值=0,标准差=1 我们后面会给出data_num=500。
 # 然后w我们看到前面的[8.1, 2, 2, 4] 说明w=4。 
 # 所以我们可以以生成500x4的输入数据,就像随机生成500个学生的4项指标
    x = torch.normal(0, 1, (data_num, len(w)))   
 # 接下来我们根据真实规律计算y  其实就是y=w1*x1+...+w4*x4+b 
 # 相当于按照真实评分标准计算每个学生的成绩。
    y = torch.matmul(x, w) + b                  
 # 添加噪声,模拟真实数据。生成和y形状相同的随机数 
 # 均值=0(平均不偏大也不偏小)标准差=0.01(波动范围很小)形状和y一样(500个数字)
 # 因为真实世界中总有各种随机因素,所以我们需要增加一个噪声。
    noise = torch.normal(0, 0.01, y.shape)         
    y += noise  #把噪声加到y上
    return x, y 

接下来我们用几个变量来存储真实的正确值,这样我们后面才方便用训练的值与真实值进行比较。

num = 500 #500个数据
true_w = torch.tensor([8.1,2,2,4]) #真实的w
true_b = torch.tensor(1.1) #真实的b

这里我们先看一下我们模拟的数据长什么样子

X, Y = create_data(true_w, true_b, num) #用X表示w 用Y表示b
plt.scatter(X[:, 3], Y, 1)  # 只看第4个特征(索引3)和Y的关系 用散点图来看
plt.title('这是我画的散点图')
plt.show()

有时候我们进行深度学习的时候数据量很大,不方便一次处理完。比如有500个数据,我们可以16个为一组,一组一组的提供数据。那我们可以写一个函数来分批提供数据。

def data_provider(data, label, batchsize):       #每次访问这个函数, 就能提供一批数据
    length = len(label)
    indices = list(range(length)) #创建 [0,1,2,3,...,499] 的列表
    #我不能按顺序取  把数据打乱  例子:打乱成 [345, 12, 478, 3, ...] 
    random.shuffle(indices)
#打乱后可以1.防止模型学到数据中的顺序规律2.让每个批次的数据分布更均匀3.提高模型的泛化能力

#用循环来分批取数据
    for each in range(0, length, batchsize):
        get_indices = indices[each: each+batchsize]
        get_data = data[get_indices]
        get_label = label[get_indices]

        yield get_data,get_label  #有存档点的return

batchsize = 16  #用来表示一组有多少个

yield 和 return 的区别:

  • return:一次返回所有东西,函数结束

  • yield:返回一点东西,暂停,下次调用时继续

现在准备工作都准备好了,我们进入核心部分,进行线性回归:

#我们使用这个函数计算,用之前得到的数据通过题目给的公式进行计算后
#对应的公式 pred_y = x₁×w₁ + x₂×w₂ + x₃×w₃ + x₄×w₄ + b
#得到预测值y
def fun(x, w, b):
    pred_y = torch.matmul(x, w) + b
    return pred_y

我们得到训练的值y后,如何知道这个值的质量好坏呢? 我们可以写一个损失函数用于衡量预测好坏。

#预测值y与真的y相减后取绝对值,然后相加起来处于y的个数得到平均绝对误差。
def maeLoss(pre_y, y):
    return torch.sum(abs(pre_y - y)) / len(y)  # 平均绝对误差

如果得到损失(平均绝对误差)越小,预测越准。

mae只是损失函数中的一种。它的特点如下:

  • 简单直观:平均每个样本差多少

  • 对异常值不敏感

当我们发现误差很大的时候,我们应该想办法来优化我们的参数。

我们可以使用优化器:sgd 代表 Stochastic Gradient Descent(随机梯度下降),它的作用是更新参数,让模型越学越好。

#paras:要更新的参数列表,比如 [w_0, b_0] 
#lr:学习率(learning rate),控制每次更新的步长
def sgd(paras, lr):          #随机梯度下降,更新参数

#关闭梯度追踪 目的:更新参数时,这个操作本身不应该被记录到计算图中。我们只想更新参数值,不想计算这次更新的梯度
    with torch.no_grad(): 
        #遍历每个需要更新的参数(w 和 b) 
        for para in paras:

            #新参数 = 旧参数 - 梯度 × 学习率 
            #为什么是减号? 梯度指向损失增加最快的方向 我们要往相反方向走(减小损失)
            para -= para.grad * lr      #不能写成   para = para - para.grad*lr

            para.grad.zero_()      #使用过的梯度,归0 因为梯度是累加的!如果不清零,下一次计算梯度时会和上一次的叠加  我们希望每次计算的是当前批次的梯度
 

接下来进行初始化

lr = 0.01
w_0 = torch.normal(0, 0.01, true_w.shape, requires_grad=True)  # 随机初始化
# 参数详解:
# - mean=0      : 均值0
# - std=0.01    : 标准差0.01(很小的范围)
# - shape=true_w.shape : 和真实w形状一样,这里是(4,)
# - requires_grad=True : 需要计算梯度

b_0 = torch.tensor(0.01, requires_grad=True)
print(w_0, b_0)  # 打印出来看看,都是接近0的小随机数

然后我们使用循环不断的训练数据

epochs = 50  # 训练50轮

for epoch in range(epochs):          # 外层循环:遍历整个数据集50遍
    data_loss = 0                     # 每轮开始,重置损失累计
    for batch_x, batch_y in data_provider(X, Y, batchsize):  # 内层循环:一批批取数据
        # 训练四步曲
        pred_y = fun(batch_x, w_0, b_0)  # 1. 前向传播:预测
        loss = maeLoss(pred_y, batch_y)   # 2. 计算损失:看差多少
        loss.backward()                    # 3. 反向传播:算梯度
        sgd([w_0, b_0], lr)                # 4. 更新参数:调整
        data_loss += loss                   # 累计这一批的损失

    print("epoch %03d: loss: %.6f" % (epoch, data_loss))  # 打印这一轮的进度
# 你看到的输出:
epoch 000: loss: 223.882538  # 第1遍:损失很大,完全没学会
epoch 001: loss: 205.934250  # 第2遍:损失下降
epoch 002: loss: 187.519699  # 第3遍:继续下降
...
epoch 012: loss: 10.543273   # 第13遍:已经很小了
epoch 013: loss: 0.554712     # 第14遍:达到最低点!
epoch 014: loss: 0.460499     # 之后在小范围波动
...
epoch 049: loss: 0.492682     # 最终稳定在0.5左右

接下来可以将预测值和真实值进行对比了。

print("真实的函数值是", true_w, true_b)
print("训练得到的参数值是", w_0, b_0)

看到 w_0 接近 [8.1,2,2,4]b_0 接近 1.1,说明学习成功了!

此外,还可以进行可视化,画出拟合效果

idx = 3  # 只看第4个特征
# 画拟合直线
plt.plot(X[:, idx].detach().numpy(), 
         X[:, idx].detach().numpy()*w_0[idx].detach().numpy()+b_0.detach().numpy())
# 画原始数据点
plt.scatter(X[:, idx], Y, 1)
plt.show()

调参建议:我们可以通过修改学习率或者修改batchsize来观察不同学习率和batchsize的效果有什么区别。

#试试不同的学习率
lr = 0.01  
lr = 0.03   

这样可以明显感觉到不同参数的区别。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐