基于pytorch演练线性回归模型

引言

本文的目的是在前文基于numpy演练可视化梯度下降的代码基础上,使用pytorch来实现一个功能齐全的线性回归训练模型。

为什么仍然使用线性回归模型?

  • 线性回归模型简单,它能让我们聚集在pytorch是如何工作的,而不是模型内部的某个复杂结构或算法。
  • 与前面的[基于numpy的线性回归模型]作对比,pytorch如何让代码量更少,更易于理解。

1. 数据准备

先导入整体要用到的包。

import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
1.1 数据生成

数据生成和之前一样,这里不再赘述。

true_w = 2
true_b = 1
N = 100

np.random.seed(42)
x = np.random.rand(N, 1)
eplison = 0.1 * np.random.randn(N, 1)
y = true_w * x + true_b + eplison
x.shape, y.shape, eplison.shape
((100, 1), (100, 1), (100, 1))

数据集拆分也没什么变化。

idx = np.arange(N)
np.random.shuffle(idx)

ratio = int(0.8 * N)
x_train, x_test = x[idx[:ratio]], x[idx[ratio:]]
y_train, y_test = y[idx[:ratio]], y[idx[ratio:]]

x_train.shape, y_train.shape, x_test.shape, y_test.shape

((80, 1), (80, 1), (20, 1), (20, 1))
1.2 数据转换

主要是两方面的转换:

  • 设备:之前使用numpy时不用关心设备(numpy只支持cpu),现在使用pytorch时,需要指定设备。
  • 数据类型:矩阵转换为张量tensor。
device = 'cuda' if torch.cuda.is_available() else 'cpu'

x_train_tensor = torch.from_numpy(x_train).float().to(device)
y_train_tensor = torch.from_numpy(y_train).float().to(device)
x_test_tensor = torch.from_numpy(x_test).float().to(device)
y_test_tensor = torch.from_numpy(y_test).float().to(device)

x_train.shape, x_train_tensor.shape, y_train_tensor.shape
((80, 1), torch.Size([80, 1]), torch.Size([80, 1]))

上面将整个数据集发送到device的做法,在实际中可能存在隐患,因为数据集可能很大,如果直接发送到device上,可能会占用宝贵的显存空间。

1.3 定义数据集

在pytorch中,数据集是torch.utils.data.Dataset的子类,可以把它理解成一个元组列表,每个元组对应一个点,包含特征x、标签y。使用Dataset类需要重写几个方法:

  • init:初始化,它可以接收数据文件的路径,也可以直接接收两个张量x和y,分别表示特征和标签。
  • getitem(index):通过索引下标对数据集进行访问,可以访问单个数据点、数据切片、或者按需加载,但有一点要求是必须返回包含特征和标签的元组。
  • len:返回整个数据集的大小。

使用Dataset的好处是:可以不用一次性加载整个数据集,而是每当调用__getitem__方法时,按需加载。

并且重写了__len__和__getitem__方法。

from torch.utils.data import Dataset, DataLoader

class MyDataset(Dataset):
    def __init__(self, x_data, y_data):
        self.x = x_data
        self.y = y_data

    def __getitem__(self, index):
        return (self.x[index], self.y[index])
    
    def __len__(self):
        return len(self.x)
    
train_dataset = MyDataset(x_train_tensor, y_train_tensor)
test_dataset = MyDataset(x_test_tensor, y_test_tensor)

train_dataset[0], train_dataset[:5]
((tensor([0.7713]), tensor([2.4745])),
 (tensor([[0.7713],
          [0.0636],
          [0.8631],
          [0.0254],
          [0.7320]]),
  tensor([[2.4745],
          [1.1928],
          [2.9128],
          [1.0785],
          [2.4732]])))
1.4 定义小批量数据加载器

前面验证了小批量梯度下降,在同样的数据量下,比批量梯度更容易收敛,比随机梯度下降计算量更少,效果更稳定。小批量梯度下降最主要的工作就是选择每次训练使用多少数据量,以及使用哪部分数据集,pytorch的DataLoader类可以很方便完成这项工作,只需要为它传3个参数:

  • dataset: 数据集
  • batch_size: 小批量大小
  • shuffle: 是否打乱数据,默认是False

注:绝大多数情况下,我们都应该把shuffle设为True,以提高梯度下降性能。但是验证集和测试集其实没必要打乱,因为它们并不参与梯度计算。
注:小批量大小,通常使用2的冥,如8、16、32、64等,这样能进行内存对齐,因为CPU/GPU的内存架构通常都是按照2的冥来分配内存。

train_loader = DataLoader(train_dataset, batch_size=8, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=8, shuffle=True)
# iter函数用于获取一个迭代器,访问迭代器可以逐个获取每个数据批次
# next函数用于从迭代器中获取下一个数据批次
x, y = next(iter(train_loader))
x, y
(tensor([[0.2809],
         [0.8631],
         [0.3110],
         [0.9507],
         [0.0740],
         [0.2912],
         [0.6233],
         [0.1220]]),
 tensor([[1.5846],
         [2.9128],
         [1.5245],
         [2.8715],
         [1.1713],
         [1.4361],
         [2.2940],
         [1.2406]]))

2. 模型配置

2.1 创建参数

训练数据和参数/权重都是tensor, 但两者最大的区别在于:参数/权重是有梯度的,可以更新参数值,而requires_grad=True就是告诉pytorch,这是一个可学习的参数,需要梯度计算。

pytorch中也有与numpy中相似的api来设置随机数种子,并随机初始化参数,唯一不同的是,我们需要通过device参数将创建的参数分配到指定设备上。

torch.manual_seed(42)
w = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
w, b
(tensor([0.3367], requires_grad=True), tensor([0.1288], requires_grad=True))
2.2 定义模型

在pytorch中,模型由继承自nn.Module的类来表示,模型类最基本的两个方法是:

  1. init:定义构成模型的组成部分,包括定义参数,以及嵌套其它模型。
  2. forward(x):定义模型的前向传播过程,即实际的计算操作,给定输入x的情况下,输出一个预测。
class LinearModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.w = nn.Parameter(torch.randn(1, requires_grad=True, dtype=torch.float, device=device))
        self.b = nn.Parameter(torch.randn(1, requires_grad=True, dtype=torch.float, device=device))
    
    def forward(self, x):
        return self.w * x + self.b

torch.manual_seed(42)
model = LinearModel().to(device)
model.w, model.b
# model.state_dict
(Parameter containing:
 tensor([0.3367], requires_grad=True),
 Parameter containing:
 tensor([0.1288], requires_grad=True))

注:在使用模型进行预测时,应该调用model(x)而不是model.forward(x),原因是对整个模型的调用涉及到额外的步骤

注意到,在__init__方法中使用Parameter类包装了参数b和w,这样做的目的是可以使用模型的parameters()来检索所有模型参数,甚至包括嵌套模型的参数,而不用自己构建参数列表,这在复杂模型中非常有用。

list(model.parameters())
[Parameter containing:
 tensor([0.3367], requires_grad=True),
 Parameter containing:
 tensor([0.1288], requires_grad=True)]

此外,还可以使用模型的state_dict方法来获取所有参数的当前值。

model.state_dict()
OrderedDict([('w', tensor([0.3367])), ('b', tensor([0.1288]))])
2.3 定义损失函数

pytorch中的nn.MSELoss()函数,可以返回计算均方误差的函数lossfn。MSELoss是一个高阶函数,支持通过reduction参数来指定损失的计算方式,默认值为mean表示计算均方差。

lossfn = nn.MSELoss()
yhat = model(x_train_tensor)
loss = lossfn(yhat, y_train_tensor)
loss
tensor(3.0332, grad_fn=<MseLossBackward0>)
2.4 自动梯度

在pytorch中,梯度是不需要手动计算的,它通过反向传播函数backward()就能自动计算所有(需要梯度的)张量的梯度。

loss.backward()
model.w.grad, model.b.grad
(tensor([-1.8803]), tensor([-3.3318]))

它代替了numpy版本中手动计算梯度的代码:

b_grad = 2 * error.mean()
w_grad = 2 * (x_train * error).mean()

单从这个例子可能还不足以看出反向传播函数的好处,试想如果是包含几十个层次的神经网络,每一层的参数都去手动推导公式并计算梯度,将是多么复杂的一件事。但现在一句代码loss.backward()就将所有参数的梯度都计算好了。

loss.backward()之所以能做到这一切,是基于梯度计算的链式法则,详细请参考:动手学深度学习-求导

这里需要说明的一点是,pytorch中的梯度是会累加的,如果将上面计算梯度和反向传播的代码再运行一次,会发现梯度变成原来的2倍。

yhat = model(x_train_tensor)
loss = lossfn(yhat, y_train_tensor)
loss.backward()
model.w.grad, model.b.grad
(tensor([-3.7606]), tensor([-6.6637]))

这会带来一个问题,在使用小批量梯度下降进行训练时,第2个小批量的梯度会在第1个小批量梯度的基础上累加。但是,我们希望每个小批量上的梯度都应该是基于当前损失独立计算的,不应该使用累积梯度。

因此,我们每轮使用梯度更新参数后,需要将梯度清零。

model.w.grad.zero_()
model.b.grad.zero_()
model.w.grad, model.b.grad
(tensor([0.]), tensor([0.]))
2.5 动态计算图

动态计算图是以可视化的方式来展示模型结构和计算过程,主要用到两个软件包:torchviz和graphviz。torchviz安装比较简单,graphviz的安装参考:Mac下安装Graphviz实用教程。

torchviz的使用比较简单,只需要调用make_dot函数,并传入预测值yhat即可。

from torchviz import make_dot

make_dot(yhat)

在这里插入图片描述

  • 蓝色框对应于参数w和b,就是需要计算梯度的张量。
  • 灰色框(MulBackward0和AddBackward0)是计算梯度时,需要临时保存的中间结果。
  • 绿色框(80,1)是梯度计算起点的张量,也就是调用backward函数的loss,反向传播是自下而上的。

为什么没有一个数据框(x)呢?

原因在于,x是输入数据,不需要计算梯度。计算图只显示涉及梯度计算的张量及其依赖的计算关系。

如果我们将参数b设为不需要梯度,那么b所在的计算分支将从计算图中消失。

model.b.requires_grad_(False)

yhat = model(x_train_tensor)
make_dot(yhat)

在这里插入图片描述

测试过后,记得将参数b的requires_grad恢复为True。

2.6 定义参数优化器

就和梯度计算一样,当涉及到复杂模型中有很多参数时,手动更新参数将不现实。pytorch中提供了optim模块,里面有很多优化器可以来更新参数,我们这里就使用随机梯度下降SGD优化器。

  • 只要指定参数和学习率,再调用step方法就可以自动更新参数。
  • 调用zero_grad方法可以将所有参数的梯度置零,不再需要逐个调用每个参数梯度的_zero方法。
optimizer = optim.SGD([model.w, model.b], lr=0.2)
optimizer.step()
optimizer.zero_grad()
model.w, model.b, model.w.grad, model.b.grad
(Parameter containing:
 tensor([0.3367], requires_grad=True),
 Parameter containing:
 tensor([0.1288]),
 None,
 None)

3 训练

3.1 构建训练步骤

在训练阶段,其实就是固定的4个步骤:

  1. 计算模型预测值
  2. 计算损失值
  3. 计算梯度
  4. 更新参数

这4个步骤会在不同的迭代数据集上反复执行,所以我们有必要封装一个函数,方便我们重复使用这段逻辑。

def build_train_step(model, loss_fn, optimizer):
    def train_step(x, y):
        # 设置模型为训练模式,
        model.train()
        # 模型预测——前向传递
        yhat = model(x)
        # 计算损失
        loss = loss_fn(yhat, y)
        # 反向传播计算梯度
        loss.backward()
        # 使用梯度和学习率更新参数
        optimizer.step()
        optimizer.zero_grad()
        # 返回损失
        return loss.item()
    return train_step

注:模型、损失函数、优化器是会改变的,所以这几个将作为参数来构建训练步骤train_step, 返回的训练步骤train_step接受特征x和标签y作为参数来完成一轮训练。

3.2 构建验证步骤

验证模型的目的,是为了观察模型对从未见过数据进行预测时的错误程度。验证步骤和训练步骤非常相似,但是不需要梯度下降。

  1. 使用模型来计算预测
  2. 使用损失函数来计算损失

还有一重要区别:必须调用eval方法将模型设置评估模式。

在训练模式时,模型会自动执行一些操作(如dropout丢弃)来减少过拟合,这种操作会破坏评估,所以在评估模式时需要关掉。

def build_evaluate_step(model, loss_fn):
    def evaluate_step(x, y):
        # 设置模型为评估模式
        model.eval()
        # 计算模型的预测输出,前向传递
        y_hat = model(x)
        # 计算损失
        loss = loss_fn(y_hat, y)
        # 返回损失值
        return loss.item()
    return evaluate_step
3.3 训练循环

当封装了训练步骤后,训练循环主要就做三件事:

  1. 迭代数据
  2. 执行一个训练步骤
  3. 跟踪训练损失

而对于模型验证来说,采用边训练边验证损失更容易发现一些过拟合的问题,所以验证损失也作为训练的一部分,包括执行步骤也是上面三步,唯一的区别在于把它包裹在了torch.no_grad()中。

torch.no_grad()相当于一个上下文管理器,它能禁用任何训练阶段的操作,避免在验证阶段误触发会对模型产生影响的梯度计算,同时也节省时间和计算资源。

epoch = 100
losses, test_losses = [], []
w_history, b_history = [model.w.item()], [model.b.item()]
train_step = build_train_step(model, lossfn, optimizer)
evaluate_step = build_evaluate_step(model, lossfn)

for i in range(epoch):
    # 迭代下一个小批量数据集
    x, y = next(iter(train_loader))
    # 执行一个训练步骤
    loss_val = train_step(x.to(device), y.to(device))
    # 记录训练损失
    losses.append(loss_val)
    # 记录模型参数
    w_history.append(model.w.item())
    b_history.append(model.b.item())

    # 验证时,不更新模型参数
    with torch.no_grad():
        x, y = next(iter(test_loader))
        test_loss = evaluate_step(x.to(device), y.to(device))
        test_losses.append(test_loss)


model.state_dict(), optimizer.state_dict(), losses, test_losses, 
(OrderedDict([('w', tensor([1.9258])), ('b', tensor([1.0505]))]),
 {'state': {0: {'momentum_buffer': None}, 1: {'momentum_buffer': None}},
  'param_groups': [{'lr': 0.2,
    'momentum': 0,
    'dampening': 0,
    'weight_decay': 0,
    'nesterov': False,
    'maximize': False,
    'foreach': None,
    'differentiable': False,
    'params': [0, 1]}]},
 [2.3343870639801025,
  1.4687697887420654,
  0.15246182680130005,
  ……
  0.013301181606948376,
  0.009759355336427689],
 [1.1239256858825684,
  0.0859474241733551,
  0.14135879278182983,
  0.011405732482671738,
 ……
  0.007371845189481974])
def show_losses(losses, test_losses, w_history, b_history):
    fig, ax = plt.subplots(1, 2, figsize=(12, 6))
    epoches = range(1, len(losses) + 1)
    ax[0].plot(epoches, losses, label='train losses')
    ax[0].plot(epoches, test_losses, label='test losses')
    ax[0].set_xlabel('epoch')
    ax[0].set_ylabel('loss')
    ax[0].set_yscale('log')  # 
    ax[0].set_title('loss descent path')
    ax[0].legend()

    ax[1].plot(b_history, w_history, c='b', marker='.', linewidth=0.5, linestyle='--')
    ax[1].set_xlabel('b')
    ax[1].set_ylabel('w')
    ax[1].set_title('parameters fitting path')
    ax[1].annotate(f'Random start({b_history[0]:0.4f}, {w_history[0]:0.4f})', xy=(b_history[0]+0.1, w_history[0]), fontsize=10, color='k')
    ax[1].plot(true_b, true_w, c='k', marker='o')
    ax[1].annotate(f'True values({true_b, true_w})', xy=(true_b+0.1,  true_w-0.01), fontsize=10, color='g')
    plt.tight_layout()
    plt.show()

show_losses(losses, test_losses, w_history, b_history)

在这里插入图片描述

注:可以看到损失的y轴采用的是对数,原因在于损失的数值不是线性均匀分布,最开始的几轮训练存在少数损失较大的值,后面更多轮训练的损失都集中在了某个小区域内,如果采用线性分布会使得图形的显示效果不佳。

参考资料

  • 基于numpy演练可视化梯度下降
  • Mac下安装Graphviz实用教程
  • 动手学深度学习-求导
  • 动手学深度学习-tensor

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/843074.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用百度语音技术实现文字转语音

使用百度语音技术实现文字转语音 SpringBootVue前后端分离项目 调用api接口需要使用AK和SK生成AccessToken,生成getAccessToken的接口有跨域限制,所以统一的由后端处理了 部分参数在控制台->语音技术->在线调试里面能找到 Controller RestController RequestMapping(&q…

C++ | Leetcode C++题解之第268题丢失的数字

题目&#xff1a; 题解&#xff1a; class Solution { public:int missingNumber(vector<int>& nums) {int n nums.size();int total n * (n 1) / 2;int arrSum 0;for (int i 0; i < n; i) {arrSum nums[i];}return total - arrSum;} };

探索 Framer Motion 高级动画技巧:提升前端设计水平

在现代的网页和应用设计中&#xff0c;动画不仅仅是视觉的点缀&#xff0c;更是用户体验的重要组成部分。它能够使界面更具吸引力&#xff0c;提升交互的流畅性&#xff0c;甚至在不经意间传达品牌的个性和态度。然而&#xff0c;要创造出令人惊叹的动效并不容易——直到有了 F…

Edge侧边栏copilot消失

Edge侧边栏copilot消失 当前环境 自己ip问题已解决&#xff0c;edge中已登录账号&#xff0c;地区已设置为美国&#xff0c;语言已设置为英文。具体可以通过空白页右上角的setting验证 解决方案 首先&#xff0c;打开“任务管理器”&#xff0c;在其中找到 Microsoft Edge…

【SASS/SCSS(三)】样式的复用与动态计算(@mixin和@function)

目录 一、mixin 1、定义复用的样式代码&#xff0c;接受传参&#xff0c;搭配include使用。 位置传参 关键词传参 ...语法糖接受传入的任意参数 2、在mixin中使用content&#xff0c;获取外部对mixin的追加内容 二、function 三、字符串——值得注意的点 很多时候&#…

[Doris]阿里云搭建Doris,测试环境1FE 1BE

首先&#xff1a;阿里云的国内服务器千万不要用容器搭建&#xff0c;或者自己Dockfile构建镜像。两种方式都不得行&#xff0c;压根拉不到github的镜像&#xff0c;开了镜像加速器也拉不到&#xff0c;不要折腾了&#xff0c;极其愚蠢。 背景&#xff1a;现在测试环境&#xff…

算法力扣刷题记录 五十六【501.二叉搜索树中的众数】

前言 二叉搜索树操作&#xff0c;继续。 记录 五十六【501.二叉搜索树中的众数】 一、题目阅读 给你一个含重复值的二叉搜索树&#xff08;BST&#xff09;的根节点 root &#xff0c;找出并返回 BST 中的所有 众数&#xff08;即&#xff0c;出现频率最高的元素&#xff09;…

【BUG】已解决:zipfile.BadZipFile: File is not a zip file

已解决&#xff1a;zipfile.BadZipFile: File is not a zip file 欢迎来到英杰社区https://bbs.csdn.net/topics/617804998 欢迎来到我的主页&#xff0c;我是博主英杰&#xff0c;211科班出身&#xff0c;就职于医疗科技公司&#xff0c;热衷分享知识&#xff0c;武汉城市开发…

IAR环境下STM32+IAP方案的实现

--基于STM32F103ZET6的UART通讯实现 一、什么是IAP&#xff0c;为什么要IAP IAP即为In Application Programming(在应用中编程)&#xff0c;一般情况下&#xff0c;以STM32F10x系列芯片为主控制器的设备在出厂时就已经使用J-Link仿真器将应用代码烧录了&#xff0c;如果在设备使…

Day16_集合与迭代器

Day16-集合 Day16 集合与迭代器1.1 集合的概念 集合继承图1.2 Collection接口1、添加元素2、删除元素3、查询与获取元素不过当我们实际使用都是使用的他的子类Arraylist&#xff01;&#xff01;&#xff01; 1.3 API演示1、演示添加2、演示删除3、演示查询与获取元素 2 Iterat…

ros笔记03--从零体验ros2话题通信方式

ros笔记03--从零体验ros2话题通信方式 介绍创建步骤体验官方 talker listener 案例基于python开发发布订阅案例 注意事项说明 介绍 主题是 ros2 提供的三种主要接口方式之一&#xff0c;它通常被用于连续的数据流&#xff0c;如传感器数据、机器人状态等。 ros2 是一个强类型的…

Artix7系列FPGA实现SDI视频编解码+UDP以太网传输,基于GTP高速接口,提供工程源码和技术支持

目录 1、前言工程概述免责声明 2、相关方案推荐本博已有的 SDI 编解码方案本博已有的以太网方案本博已有的FPGA图像缩放方案本方案的缩放应用本方案在Xilinx--Kintex系列FPGA上的应用本方案在Xilinx--Zynq系列FPGA上的应用 3、详细设计方案设计原理框图SDI 输入设备Gv8601a 均衡…

SAP Fiori 实战课程(二):新建页面

课程回顾 上一课中,利用Visual studio Code 新建、并运行了一个Demo工程。可以实现对项目的启动,启动后进入一个List清单。 那么本次课程的目前就是在上一节Demo的基础上,从零开始新建一个完整的页面。实现从首页清单,选择行后,鼠标点击,进入下一个页面。 准备工作 在开…

【20】读感 - 架构整洁之道(二)

概述 继上一篇文章讲了前两章的读感&#xff0c;已经归纳总结的重点&#xff0c;这章会继续跟进的看一下&#xff0c;深挖架构整洁之道。 编程范式 编程范式从早期到至今&#xff0c;提过哪些编程范式&#xff0c;结构化编程&#xff0c;面向对象编程&#xff0c;函数式编程…

想要获客如有神助攻,宝藏工具必不可少!

现如今&#xff0c;客户资源的收集和管理成为了一个让很多人都为之烦恼的问题。 然而&#xff0c;随着科技的进步&#xff0c;市场上出现了许多高效的宝藏工具&#xff0c;可以帮助你轻松解决这些问题&#xff0c;让获客如有神助攻&#xff01; 1、丰富的客户来源 无论是附近…

TypeScript体操(二):Utility Type手写实现

目录 前言常用 Utility Types 及其实现Partial<T>Required<T>Readonly<T>Pick<T, K>Omit<T, K>Record<K, T>Exclude<T, U>Extract<T, U>NonNullable<T>ReturnType<T>InstanceType<T>Parameters<T>Con…

synergy配置

今天介绍一个电脑同步软件synergy。 我们开发时一般会用两套设备&#xff0c;如果使用两套键盘操作起来会很麻烦&#xff0c;这个软件就是解决这个问题&#xff0c;可以使用一套键盘同时操作两台电脑&#xff0c;另一台作为客户端被控制。 安装 在两台电脑上各自下载安装syne…

沃文特过会两年仍在注册:营收净利润下滑,三次抽查不合格

《港湾商业观察》施子夫 王璐 在当前IPO严查之际&#xff0c;部分企业的上市进程可谓相当漫长&#xff0c;四川沃文特生物工程股份有限公司&#xff08;以下简称&#xff0c;沃文特&#xff09;就是其中之一。 不过&#xff0c;最近的好消息是&#xff0c;沃文特又更新了招股…

NVIDIA Container Toolkit 安装与配置帮助文档(Ubuntu,Docker)

NVIDIA Container Toolkit 安装与配置帮助文档(Ubuntu,Docker) 本文档详细介绍了在 Ubuntu Server 22.04 上使用 Docker 安装和配置 NVIDIA Container Toolkit 的过程。 概述 NVIDIA 容器工具包使用户能够构建和运行 GPU 加速容器。即可以在容器中使用NVIDIA显卡。 架构图如…

【BUG】已解决:TypeError: Descriptors cannot not be created directly.

已解决&#xff1a;TypeError: Descriptors cannot not be created directly. 目录 已解决&#xff1a;TypeError: Descriptors cannot not be created directly. 【常见模块错误】 【错误原因】 【解决方案】 欢迎来到英杰社区https://bbs.csdn.net/topics/617804998 欢迎来…