人工智能入门(一):基于Pytorch的手写数字识别模型

news2025/1/19 7:54:01

前言:

因为还在上学,时间不太够用,很多内容写到后面心有余力不足,未来有时间我会慢慢补充。人工智能的知识涉猎范围广又杂乱无章,啃书或上课学到的知识往往很早就过时了或者离实际的项目无关。所以,我很希望通过这种一个一个“小实验”的方式让大家拎出一个属于自己的“主干知识”,尝到训练模型的“甜头”。

一、Python基础知识:

1 类与对象的关系:

比如定义一个“鸟类”。鹦鹉就是“鸟类”的一个对象,海鸥也是“鸟类”的一个对象。

“类”是静态的,是在程序执行前就已经被定义好的;

“对象”是动态的,它们在程序执行时可以被创建和删除。

2 类的属性:

class Bird:
    Flight_mode = '滑翔'
    Reproduction_mode = '蛋生'  #类的属性
    def introduce(self):   #类的方法
        print('爷是只高贵的鸟')

类的属性是全类共有的,比如上例中定义的“鸟类“里的”飞行方式“和”繁衍方式“;

类的方法可以简单理解成在类中写了一个函数,不过要注意的是:在Python中,self是一个表示“对象“自身的参数,通常作为方法的第一个参数。当我们创建一个对象时,Python会自动将该对象作为self参数传递给类的方法。

接下来,请读者思考一个问题,假如我有一只小鸟名叫”Lucky“,基于上面的例子,如果我想打印:

(1)“Lucky的飞行方式是滑翔,繁衍方式是蛋生”应该怎么做?

(2)“Lucky的年龄是一岁大,颜色是黄色的”上例中类的方法应该怎么修改?

解答:

(1)的问题比较简单,因为飞行方式和繁衍方式都是整个“鸟类”共同的属性,直接使用类名调用即可。

print("Lucky的飞行方式是"+Bird.Flight_mode+",繁衍方式是"+Bird.Reproduction_mode)

(2)最大的问题在于,年龄和颜色都是Lucky这个“对象”的属性,这时我们就需要使用类的方法来定义对象的属性。说白了就是在类中写一个函数,让函数中的属性只属于定义的对象。

class Bird:
    Flight_mode = '滑翔'
    Reproduction_mode = '蛋生'  # 类的属性

    def __init__(self, name, age, colour):
        self.name = name
        self.age = age
        self.colour = colour

    def introduce(self):
        print(self.name,"的年龄是", self.age, ",颜色是", self.colour)

p = Bird("Lucky", "一岁大", "黄色的")
p.introduce()

其中,__init__是一个特殊的方法,用于在创建对象时进行初始化。它是Python中的构造函数,用于将属性值赋给对象。

类的基础介绍言尽于此,不多赘述。

参考文章:http://t.csdnimg.cn/Eg9kr

3 类的继承:

继承的主要作用是实现代码的重用。继承使得子类拥有父类的方法和属性。在使用深度学习框架,比如Pytorch时,只有将我们的类继承官方的类,我们才能在官方的类实现的基础上继续做下去

class animal:
	def eat(self):
		print("吃")
		
	def drink(self):
		print("喝")
	
class dog(animal):
	def dark(self):
		print("汪汪叫")
	

goudan = dog()
goudan.eat()
goudan.drink()

从上面的代码可以看出,在编写dog类的时候,我们并没有重写eat和drink两个方法。我们只需要在dog后面的括号中加上父类的名字即可。当子类继承了父类,子类就可以直接使用父类中的方法了。在本例中,goudan可以直接使用animal类中的eat和drink两个方法。

参考文章:http://t.csdnimg.cn/sw0VK

二、简单科普:

1 深度学习框架的概念:

如何让计算机识别手写数字0~9?如今的我们只需要一个简单的卷积神经网络(CNN)就可以搞定。

本次我们使用pytorch深度学习框架,使用pytorch中的nn函数来调用深度学习中的基本单元,用pytorch中的nn.squential模块来构建神经网络模型。

深度学习框架的基本属性:

1.将繁琐的计算、操作封装成黑箱,让我们只需要关注输入与输出;

2.把解决问题的流程模块化、模板化;

3.常用的工具封装成库,方便使用。

所以,框架的概念就是模板的概念,就像英语作文模板往往会给你提供好开头、结尾等,让我们只需要根据不同的作文主题更换不同的英语单词一样。

2 张量的概念:

张量,英文为“tensor”,是神经网络的主要数据容器,它包含的数据几乎都是数值。但显然,我们传入神经网络的数据并不只有数字,还有图片、声音、文本等等。如何建立起他们之间的联系?这个我们稍后解释,现在我们先来看看什么是张量。

2.1 什么是张量:

学习过线性代数的话,你可能对矩阵比较熟悉,张量其实就是矩阵向任意维度的推广

(1)如果一个矩阵只有一个元素,那么它被称为0阶张量;

(2)如果一个矩阵只有一行或者一列(我们称其为只有一个“”),则被称为一阶张量;

(3)显然,大部分的矩阵都有m行n列(有两个轴),则就被称为二阶张量;

(4)如果将一个二阶张量看作一个整体,由多个二阶张量组成一个新的数组,则二阶张量就被推广为三阶张量;

bdd298739a984edbb862b385d2071dc7.png

当然,我本人有个小技巧帮助你推导三阶之后的张量。来,跟我念:一个张量、往下拉、往右拉、往后拉;看成一个整体、往下拉(四阶)、往右拉(五阶)、往后拉(六阶);看作一个整体.......以此类推。

当然,在实践中,我们很少遇到三阶之后的张量,所以也没必要对想象出高阶张量那么执着。

参考文章:http://t.csdnimg.cn/OjOqS

2.2 图片与张量的相互转换:

首先,我们来思考一种最简单的情况。假如有一张灰度图像,显然它是一个二维的平面,它由密密麻麻的像素组成。对于每一个像素点如果我们用由低到高的数字来代表像素点灰色的深度,那么显然,这张图片可以被一个二阶张量所表示;

对于一张彩色图像,实际上每一个像素点的颜色是由“红、绿、蓝”(RGB)三种颜色组合而成。所以,在我们的肉眼看来,一张彩色图像是一个二维平面。但实际上,它是由一张红色图片、一张绿色图片、一张蓝色图片叠加而成的。仿照前面对一张灰度图像用二阶张量来表示,一张彩色就可以用一个三阶张量来表示。

以上只是这个转换过程在我们大脑中的想象,实际通过代码来实现这个过程我们需要借助Pytorch中的torchvision库来实现。

三、实战环节:

1 数据预处理:

当我们进行数据预处理时,归一化是一种常见的操作。它的目的是将数据调整到一个特定的范围或分布,通常是为了让模型更容易学习并且提高训练的稳定性和效率。

归一化的过程就好像把数据放进一个标准的框架中,使得它们的分布更加均匀、方便处理。最常见的归一化方法之一是将数据缩放到 0 到 1 的范围内。举个例子,如果你有一组身高数据,其中最小值是 150 厘米,最大值是 190 厘米,那么归一化之后,150 厘米的数据会变成 0,190 厘米的数据会变成 1,其他的数据会按比例变化到 0 和 1 之间。

而对于图像数据,我们归一化处理的其实是每一个像素点。例如,如果你有一组图像数据,像素值范围在 0 到 255 之间,那么归一化可以将像素值除以 255,将其缩放到 0 到 1 的范围内。

除了将数据缩放到 0 到 1 的范围外,还有其他一些归一化方法,如将数据缩放到均值为 0、标准差为 1 的正态分布中。不同的归一化方法适用于不同的情况,但它们的共同目标都是使数据更易于处理和学习。

小结:

1 归一化的方法:

(1)将数据按比例缩放到0~1之间;

(2)将数据缩放为均值为0,标准差为1的正态分布中。

2 图片数据的归一化是对其每一个像素点做归一化。

代码实现:

import torchvision.transforms as transforms
transform = transforms.Compose([
    transforms.ToTensor(),  # 将图像转换为张量
    transforms.Normalize((0,), (1,))  # 归一化,均值为0,标准差为1
])

2 数据加载

2.1 Dataset:

“torchvision.datasets”是 PyTorch 中用于加载常见视觉数据集的模块。

通常情况下,我们需要自定义一个Dataset类来加载我们自己的数据,该类需要实现__len__和__getitem__方法,分别用于返回数据集的长度和访问数据集中的元素。

但Pytorch中提供了torchvision.datasets.MNIST 类,它已经实现了 Dataset 接口,可以直接用于加载 MNIST 数据集。常用的数据集包括 MNIST、CIFAR-10、CIFAR-100、ImageNet 等。

接下来,我们以Minist数据集为例,对其数据进行加载与预处理:

MINIST

Size: 28×28 灰度手写数字图像
Num: 训练集 60000 和 测试集 10000,一共70000张图片
Classes: 0,1,2,3,4,5,6,7,8,9

代码实现:

import torch
from torch.utils.data import Dataset, DataLoader
# 加载训练集和测试集
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)
  1. root='./data': 这是指定存储数据集的根目录。在这个例子中,MNIST 数据集将被下载并存储在名为 data 的文件夹中。

  2. train=False: 这是一个布尔值参数,用于指示加载的是否是训练集。在这里,由于我们加载的是测试集,因此将其设为 False

  3. download=True: 这也是一个布尔值参数,用于指示是否下载数据集。如果数据集不存在,则将其设置为 True 将自动下载数据集。一旦下载完成,就会被保存在指定的 root 目录中。

  4. transform=transform: 这是数据转换的参数,用于将加载的图像数据转换为模型可接受的张量格式,并进行归一化处理。在这里,你将之前定义的 transform 应用于加载的测试集数据。

Dataset类负责从数据源中加载数据,并对数据进行预处理(如归一化、数据增强等),但它并不负责将数据组织成批次或进行随机化

2.2 Dataloader:

Dataset负责表示数据集和进行数据预处理,而DataLoader负责将数据组织成批次并加载到模型中进行训练。它接收一个“Dataset”对象作为参数,并根据指定的批量大小、是否打乱数据等参数,在实际使用中,通常会将自定义的Dataset对象传递给DataLoader进行数据加载和批处理(用上文提到的类的继承操作)。

代码实现:

import torch
from torch.utils.data import Dataset, DataLoader
# 创建数据加载器
batch_size = 4
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True,num_workers=0)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False, num_workers=0)
  1. batch_size: 这是指定每个批次(batch)包含的样本数量。在这个例子中,将每个批次设置为包含 4 个样本。

  2. shuffle=False: 这是一个布尔值参数,用于指示是否对数据进行洗牌操作。在测试集中,通常不需要对数据进行洗牌,因此将其设置为 False

  3. num_workers=0: 这是用于数据加载的子进程数量。设置为 0 表示所有数据加载操作都在主进程中进行,没有额外的子进程。在一般情况下,将其设置为大于 0 的值可以加速数据加载,但在某些环境中可能会出现问题,因此这里将其设置为 0,表示只使用主进程进行数据加载操作。

3 构建神经网络模型:

# 构建神经网络模型
class CNN(nn.Module):
    def __init__(self):
        super(CNN, self).__init__()
        self.flatten = nn.Flatten()
        self.feature_extraction = nn.Sequential(
            nn.Linear(28*28, 16*16),
            nn.ReLU(),
            nn.Linear(16*16, 8*8),
            nn.ReLU(),
            nn.Linear(8*8, 10),
            nn.ReLU()
        )

    # 下面定义x的传播路线
    def forward(self, x):
        x = self.flatten(x)
        output = self.feature_extraction(x)
        return output

在神经网络的构建代码中,注意到输入是28×28 的灰度手写数字图像,输出是0~9的十个数字,经过了三层全连接层来一步步提取特征。

在前行传播代码中,( x ) 是一个形参,表示输入数据。当模型进行训练或推理时,需要将实际的数据传递给模型,这个数据就会被赋值给 ( x )。

4 损失函数与优化器:

# 定义损失函数和优化器
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)

上例中使用交叉熵损失函数和 SGD 优化器,都是一些固定的格式。不过要注意一 点就是这个学习率,lr=1e-3 是学习率的设置,控制着每次参数更新的步长大小。 学习率是优化算法中一个重要的超参数,它决定了模型在参数空间中搜索的速度 和精度。 例如,lr=1e-3 表示学习率为 0.001,即每次参数更新时,参数值会按照梯度 的方向移动一个较小的步长。

5 定义训练函数与测试函数:

# 定义训练函数
def train(train_dataloader1, model1, loss_fn1, optimizer1):
    for batch, (X, y) in enumerate(train_dataloader1):
        X, y = X.to(device), y.to(device)

        pred = model1(X)
        loss = loss_fn1(pred, y)

        optimizer1.zero_grad()
        loss.backward()
        optimizer1.step()

        if batch % 100 == 0:
            size = len(train_dataloader1)
            loss, current = loss.item(), batch * len(X)
            print(f"loss: {loss:.7f} [{current:>5d}/{size:>5d}]")


# 定义测试函数
def test(test_dataloader2, model2):
    size = len(test_dataloader2.dataset)
    model2.eval()
    test_loss, correct = 0, 0
    with torch.no_grad():
        for X, y in test_dataloader2:
            X, y = X.to(device), y.to(device)
            pred = model(X)
            test_loss += loss_fn(pred, y).item()
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()
    test_loss /= size
    correct /= size
    print(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")

6 主程序:

# 主程序
epochs = 5
for t in range(epochs):
    print(f"Epoch {t+1}\n")
    train(train_dataloader, model, loss_fn, optimizer)
    test(test_dataloader, model)

6 模型的保存与评估:

# 保存模型
torch.save(model.state_dict(), "./model")
print("模型保存在model文件夹内")

# 加载模型
model = CNN()
model.load_state_dict(torch.load("./model"))
model.eval()  # 将模型设置为评估模式

当调用model.eval()时,就像正在告诉模型:“现在我们要用你来做测试或者预测了,所以请你把自己整理好,别再做什么让结果不确定的事情了。”就好像是在考试前整理心情一样,你希望自己在考试时保持冷静和准确,不要因为紧张或者分心而出错。

四、完整代码:

# 导入库
import torch
import torchvision
# 名称简化
import torch.nn as nn
import torchvision.transforms as transforms
from torch.utils.data import DataLoader

# 下载手写数字数据库
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])

train_dataset = torchvision.datasets.MNIST(root='./data', train=True, download=True, transform=transform)
train_dataloader = DataLoader(train_dataset, batch_size=4, shuffle=True, num_workers=0)

test_dataset = torchvision.datasets.MNIST(root='./data', train=False, download=True, transform=transform)
test_dataloader = DataLoader(test_dataset, batch_size=4, shuffle=False, num_workers=0)


# 构建神经网络模型
class CNN(nn.Module):
    def __init__(self):
        super(CNN, self).__init__()
        self.flatten = nn.Flatten()
        self.feature_extraction = nn.Sequential(
            nn.Linear(28*28, 16*16),
            nn.ReLU(),
            nn.Linear(16*16, 8*8),
            nn.ReLU(),
            nn.Linear(8*8, 10),
            nn.ReLU()
        )

    # 下面定义x的传播路线
    def forward(self, x):
        x = self.flatten(x)
        output = self.feature_extraction(x)
        return output


device = torch.device('cuda')  # 将device指定为GPU
model = CNN().to(device)  # 将模型移动到GPU上
print(model)

# 定义损失函数和优化器
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)


# 定义训练函数
def train(train_dataloader1, model1, loss_fn1, optimizer1):
    for batch, (X, y) in enumerate(train_dataloader1):
        X, y = X.to(device), y.to(device)

        pred = model1(X)
        loss = loss_fn1(pred, y)

        optimizer1.zero_grad()
        loss.backward()
        optimizer1.step()

        if batch % 100 == 0:
            size = len(train_dataloader1)
            loss, current = loss.item(), batch * len(X)
            print(f"loss: {loss:.7f} [{current:>5d}/{size:>5d}]")


# 定义测试函数
def test(test_dataloader2, model2):
    size = len(test_dataloader2.dataset)
    model2.eval()
    test_loss, correct = 0, 0
    with torch.no_grad():
        for X, y in test_dataloader2:
            X, y = X.to(device), y.to(device)
            pred = model(X)
            test_loss += loss_fn(pred, y).item()
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()
    test_loss /= size
    correct /= size
    print(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")


# 主程序
epochs = 5
for t in range(epochs):
    print(f"Epoch {t+1}\n")
    train(train_dataloader, model, loss_fn, optimizer)
    test(test_dataloader, model)

# 保存和恢复网络权值
torch.save(model.state_dict(), "./model")
print("模型保存在model文件夹内")

# 加载模型
model = CNN()
model.load_state_dict(torch.load("./model"))
model.eval()  # 将模型设置为评估模式

 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Git教程】(十七)发行版交付 — 概述及使用要求,执行过程及其实现,替代解决方案 ~

Git教程 发行版交付 1️⃣ 概述2️⃣ 使用要求3️⃣ 执行过程及其实现3.1 预备阶段:创建 stable 分支3.2 预备并创建发行版3.3 创建补丁 4️⃣ 替代解决方案 对于每个项目或产品来说,发布版本的创建都需要一定的时间,其具体过程因各公司或组…

arm版Linux下安装大数据集群各种组件

背景:由于本人是用的Macbookpro m2来进行开发的,很多环境和Intel芯片的都不一样,期间安装各种软件遇到各种问题,为了以后不走之前的老路,现记录各种软件的安装步骤。 系统安装组件说明 序号组件名称组件版本1jdkjdk-…

2024年电子商务与大数据经济国际会议 (EBDE 2024)

2024年电子商务与大数据经济国际会议 (EBDE 2024) 2024 International Conference on E-commerce and Big Data Economy 【会议简介】 2024年电子商务与大数据经济国际会议即将在厦门召开。本次会议旨在汇聚全球电子商务与大数据经济领域的专家学者,共同探讨电子商务…

基于MaxKB搭建一个知识库问答系统

什么是MaxKB MaxKB 是一款基于 LLM 大语言模型的知识库问答系统。MaxKB Max Knowledge Base,旨在成为企业的最强大脑。 开箱即用:支持直接上传文档、自动爬取在线文档,支持文本自动拆分、向量化,智能问答交互体验好&#xff1b…

深度学习| 注意力机制

注意力机制 为什么需要注意力机制Seq2Seq问题Transfomer Attention注意力机制分类软硬注意力注意力域 为什么需要注意力机制 这个可以从NLP的Seq2Seq问题来慢慢理解。 Seq2Seq问题 Seq2Seq(Sequence to Sequence):早期很多模型中&#xff…

扭蛋机小程序对市场的发展有哪些推动作用?

近几年,扭蛋机发展的非常迅猛。随着二次元文化的火热,给扭蛋机带来了发展机遇,扭蛋机行业也受到了大众的喜爱。扭蛋机的商品种类多样化,包含了各类热门IP周边衍生品、玩具、小商品等,适合所有消费人群,市场…

2024年汉字小达人活动还有5个月开赛:来做18道历年选择题备考吧

现在距离2024年第11届汉字小达人比赛还有五个多月的时间,如何利用这段时间有条不紊地备考呢?我的建议是两手准备:①把小学1-5年级的语文课本上的知识点熟悉,重点是字、词、成语、古诗。阅读理解不需要。②把历年真题刷刷熟&#x…

【漏洞复现】云时空社会化商业ERP系统slogin SQL注入漏洞

漏洞描述: 云时空社会化商业ERP系统slogin存在SQL注入漏洞,攻击者可以通过此漏洞获取数据库敏感信息。 搜索语法: Fofa-Query: app"云时空社会化商业ERP系统" 漏洞详情: 1.云时空社会化商业ERP系统。 2.漏洞POC: …

浏览器和nodejs中的eventloop

浏览器和nodejs中的eventloop 浏览器中的Event Loop 在浏览器中,设计成为了单线程。如果要处理异步请求,则需要增加一层调度逻辑,把js代码封装成一个个的任务,放在一个任务队列中,主线程不断的读取任务执行。每次调取…

IDEA2023版本创建Sping项目无法使用Java8

1. 问题复现 1.1 当前版本2023.3.2 1.2 创建项目时:不存在jdk8选项 提示报错 1.3 原因分析 Spring官方发布Spring Boot 3.0.0 的时候告知了一些情况,Java 17将成为未来的主流版本 2. 如何解决 2.1 替换创建项目的源 我们只知道IDEA页面创建Spring项目…

玩转PyCharm

玩转PyCharm PyCharm是由JetBrains公司开发的提供给Python专业的开发者的一个集成开发环境,它最大的优点是能够大大提升Python开发者的工作效率,为开发者集成了很多用起来非常顺手的功能,包括代码调试、高亮语法、代码跳转、智能提示、自动补…

历史遗留问题-Oracle 19c RAC 安装时节点连接性问题

测试服务器的二节点数据库宕掉了,原因不明,需要产环境重新安装。我想上次在自己虚拟机安装实验过一次,应该一天能搞定,事实证明,你永远有学不完的bug!!!! 首先查看一下系…

python 调试 c++源码

1. gdb常用调试命令概览和说明 2. 编译c库设置Debug模式 cmake设置debug 在CMake中设置debug模式通常意味着启用调试信息和优化。以下是一个简单的CMakeLists.txt文件示例,展示了如何设置项目以便在Debug模式下构建: cmake_minimum_required(VERSION 3…

【WEEK9】 【DAY3】JSR303数据校验及多环境切换【中文版】

2024.4.24 Wednesday 目录 4.JSR303数据校验及多环境切换4.1.JSR303数据校验(了解即可)4.1.1.修改Person.java4.1.2.修改pom.xml(添加依赖)4.1.3.运行Springboot02ConfigApplicationTests.java进行测试4.1.4.使用数据校验&#x…

HotSpot JVM 中的应用程序/动态类数据共享

0.前言 本文的目的是详细讨论 HotSpot JVM 自 JDK 1.5 以来提供的一项功能,该功能可以减少启动时间,但如果在多个 JVM 之间共享相同的类数据共享 (CDS) 存档,则还可以减少内存占用。 1.类数据共享 (CDS) CDS 的想法是使用特定格式将预处理…

【氮化镓】液态Ga在GaN(0001)和(0001̅)表面上的三维有序排列随温度的变化

文章标题是《Temperature dependence of liquid-gallium ordering on the surface of epitaxially grown GaN》,作者是Takuo Sasaki等人,发表在《Applied Physics Express》上。文章主要研究了在分子束外延(MBE)条件下,液态镓(Ga)在GaN(0001)…

探索在Apache SeaTunnel上使用Hudi连接器,高效管理大数据的技术

Apache Hudi是一个数据湖处理框架,通过提供简单的方式来进行数据的插入、更新和删除操作,Hudi能够帮助数据工程师和科学家更高效地处理大数据,并支持实时查询。 支持的处理引擎 Spark Flink SeaTunnel Zeta 主要特性 批处理 流处理 精确一次性…

状态模式和策略模式对比

状态模式和策略模式都是行为型设计模式,它们的主要目标都是将变化的行为封装起来,使得程序更加灵活和可维护。之所以将状态模式和策略模式进行比较,主要是因为两个设计模式的类图相似度较高。但是,从状态模式和策略模式的应用场景…

2024最新版JavaScript逆向爬虫教程-------基础篇之深入JavaScript运行原理以及内存管理

目录 一、JavaScript运行原理1.1 前端需要掌握的三大技术1.2 为什么要学习JavaScript1.3 浏览器的工作原理1.4 浏览器的内核1.5 浏览器渲染过程1.6 认识JavaScript引擎1.7 V8引擎以及JavaScript的执行过程1.8 V8引擎执行过程 二、JavaScript的执行过程2.1 初始化全局对象2.2 执…

解决宏定义后面无法加分号

总结:注意是针对单行if语句使用,并且宏定义后面必须带分号(格式统一) 参考连接 C语言种do_while(0)的妙用_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1vk4y1R7VJ/?spm_id_from333.337.search-card.all.click&vd_…