Rust机器学习之tch-rs

news2024/11/19 23:40:05

Rust机器学习之tch-rs

tch-rs是PyTorch接口的Rust绑定,可以认为tch-rs是Rust版的PyTorch。本文将带领大家学习如何用tch-rs搭建深度神经网络识别MNIST数据集中的手写数字。

本文是“Rust替代Python进行机器学习”系列文章的第五篇,其他教程请参考下面表格目录:

Python库Rust替代方案教程
numpyndarrayRust机器学习之ndarray
pandasPolars Rust机器学习之Polars
scikit-learnLinfaRust机器学习之Linfa
matplotlibplottersRust机器学习之plotters
pytorchtch-rsRust机器学习之tch-rs
networkspetgraphRust机器学习之petgraph

数据和算法工程师偏爱Jupyter,为了跟Python保持一致的工作环境,文章中的示例都运行在Jupyter上。因此需要各位搭建Rust交互式编程环境(让Rust作为Jupyter的内核运行在Jupyter上),相关教程请参考 Rust交互式编程环境搭建

在这里插入图片描述

文章目录

    • 初识tch-rs
      • PyTorch vs. TensorFlow
      • 安装tch-rs
    • 用tch-rs搭建简单神经网络
      • 环境准备
      • 实现思路
      • 准备数据
      • 转成Tensor
      • 构建模型
    • 用tch-rs搭建序贯神经网络
    • 用tch-rs搭建卷积神经网络
    • 总结

初识tch-rs

PyTorch vs. TensorFlow

在深度学习领域,最受欢迎的开源框架非TensorFlow和PyTorch莫属。这两个框架都为构建和训练深度学习模型提供了广泛的功能,并已被研发社区广泛采用。目前二者无论从功能还是性能都非常接近,但PyTorch的接口设计更加“pythonic”且支持面向对象,相比之下,虽然TensorFlow提供更多选择给开发者,但接口和设计模式稍显混乱。因此,尽管TensorFlow诞生较早,但近年来PyTorch越来越受欢迎,已经超过TensorFlow。下图是谷歌趋势绘制的二者近5年的搜索趋势:

在这里插入图片描述

图1. TensorFlow vs. PyTorch
PyTorch已经超过TensorFlow成为最受欢迎的开源深度学习框架

tch-rs是PyTorch接口的Rust绑定,可以认为tch-rs是Rust版的PyTorch。tch-rs由Laurent Mazare 开发,是目前最Rustacean的PyTorch绑定,它对C++实现的libtorch进行了很薄的一层封装,这样做的最大优势是封装库与原始库严格相似,从而极大地降低了学习成本。如果你对PyTorch非常熟悉,几乎可以毫不费力得迁移到tch-rs上。

安装tch-rs

安装使用tch-rs非常简单,只需要在Cargo .toml加入

[dependencies]
tch = "0.8.0"

在机器学习中,我们更喜欢使用Jupyter。如果你已经搭建好Rust交互式编程环境(可以参考 《Rust交互式编程环境搭建》),可以直接通过下面代码引入tch-rs :

:dep tch = {version="0.8.0"}

初次编译tch-rs时间会有点长。但好在jupyter中cell之间是共享环境的,第一次编译加载完后,后面调用都很快。

用tch-rs搭建简单神经网络

环境准备

我们首先在MNIST数据集上训练一个简单得神经网络,为此我们需要mnist包来下载MNIST数据集(MNIST数据集的版权归Yann LeCun 和 Corinna Cortes所有,我们可以在 Creative Commons Attribution-Share Alike 3.0证书下获取使用),同时还需要引入ndarray包来对图片向量数据进行一些转换操作,并最终将其转换成tch::Tensor类型。(关于ndarray的使用请参考《Rust机器学习之ndarray》)。

:dep mnist = {version = "0.5.0", features = ["download"]}
:dep ndarray = {version = "0.15.6"}
use mnist::*;
use ndarray::prelude::*;

实现思路

要完成这个神经网络的搭建,我们需要分三步:

  1. 下载并解压MNIST数据集,并将数据集中的图片转换为向量,共训练、验证和测试使用;
  2. 将向量转换为Tensor类型,因为tch-rs的输入数据类型为Tensor类型;
  3. 实现一系列迭代,每次迭代我们将输入数据和神经网络权重矩阵相乘,然后执行反向传播算法更新权重值。

我们下面一步一步来实现。

准备数据

mnist包中的MnistBuilder结构封装了下载、解压、加载、拆分等一系列数据准备工作,我们可以通过下面代码完成数据准备工作:

const TRAIN_SIZE: usize = 50000;
const VAL_SIZE: usize = 10000;
const TEST_SIZE: usize =10000;

let Mnist {
    trn_img,
    trn_lbl,
    val_img, 
    val_lbl,
    tst_img,
    tst_lbl,
} = MnistBuilder::new()
    .download_and_extract()
    .label_format_digit()
    .training_set_length(TRAIN_SIZE as u32)
    .validation_set_length(VAL_SIZE as u32)
    .test_set_length(TEST_SIZE as u32)
    .finalize();
  • download_and_extract():下载并解压MNIST数据集,该方法需要启用download特性
  • label_format_digit():将标签格式设为标量数字
  • training_set_length(TRAIN_SIZE as u32):拆分训练集
  • validation_set_length(VAL_SIZE as u32):拆分验证集
  • test_set_length(TEST_SIZE as u32):拆分测试集
  • finalize():根据上面的配置获取数据(Mnist结构类型)

返回值Mnist结构包含多个数据子集,在机器学习任务中,通常包含如下3类数据:

  • 训练集 - 用于训练模型
  • 验证集 - 用于训练过程中验证模型效果(MNIST默认数据分割中不包含验证集)
  • 测试集 - 用于训练后评估模型表现

每个子集包含2个向量,一个向量保存图片数据,另一个向量保存标签。向量中的数据都是”平展“的,假如有 60 , 000 60,000 60,000张图片,那么向量中将包含 60 , 000 × 28 × 28 = 47 , 040 , 000 60,000 \times 28 \times 28 = 47,040,000 60,000×28×28=47,040,000个元素,其中 28 28 28是图片行列的像素数。

MNIST数据集包含70,000张手写数字图片和其对应标签。每张照片 28 × 28 28 \times 28 28×28像素,灰度值0到255。标签是图片对应的数字0到9。默认情况下60,000张划为训练集,10,000张划为测试集。

转成Tensor

use tch::{kind, no_grad, Kind, Tensor};

pub fn image_to_tensor(data:Vec<u8>, dim1:usize, dim2:usize, dim3:usize)-> Tensor{
    // 将Vec转换为三维数组并将颜色值进行归一化处理 
    let inp_data: Array3<f32> = Array3::from_shape_vec((dim1, dim2, dim3), data)
        .expect("Error converting data to 3D array")
        .map(|x| *x as f32/256.0);
    // 转成Tensor
    let inp_tensor = Tensor::of_slice(inp_data.as_slice().unwrap());
    // 将Tensor转换成 [dim1, dim2*dim3] 结构的张量
    let ax1 = dim1 as i64; 
    let ax2 = (dim2 as i64)*(dim3 as i64);
    let shape: Vec<i64>  = vec![ ax1, ax2 ];
    let output_data = inp_tensor.reshape(&shape);
    println!("Output image tensor size {:?}", shape);
        
    output_data
}

上面的代码利用from_shape_vec将输入的Vec<u8>类型数据转换成Array3.map(|x| *x as f32/256.0)对数值进行了归一化,并转换成f32类型。tch-rs提供了Tensor::of_slice方法,可以方便地将数组转换为torch Tensor类型。输出张量的大小为 d i m 1 × ( d i m 2 × d i m 3 ) dim1 \times (dim2 \times dim3) dim1×(dim2×dim3),分别对应我们的训练数据集TRAIN_SIZE = 50000HEIGHT = 28WIDTH = 28,因此输出张量的大小为 50000 × ( 28 × 28 ) = 50000 × 784 50000 \times (28 \times 28) = 50000 \times 784 50000×(28×28)=50000×784

同理,我们需要将标记数据也转成Tensor,它的大小为dim1——因此,对应训练集标记数据我们需要一个大小为50000的张量。代码如下:

pub fn labels_to_tensor(data:Vec<u8>, dim1:usize, dim2:usize)-> Tensor{
    let inp_data: Array2<i64> = Array2::from_shape_vec((dim1, dim2), data)
        .expect("Error converting data to 2D array")
        .map(|x| *x as i64);

    let output_data = Tensor::of_slice(inp_data.as_slice().unwrap());
    println!("Output label tensor size {:?}", output_data.size());
    
    output_data
}

构建模型

现在,我们可以开始着手构建我们的线性神经网络模型了。

首先我们将权重矩阵和误差矩阵设为0:

let mut ws = Tensor::zeros(&[(HEIGHT*WIDTH) as i64, LABELS], kind::FLOAT_CPU).set_requires_grad(true);
    let mut bs = Tensor::zeros(&[LABELS], kind::FLOAT_CPU).set_requires_grad(true);

然后循环迭代训练线性神经网络

const LABELS: i64 = 10; // 标签类别数量
const HEIGHT: usize = 28; 
const WIDTH: usize = 28;
const N_EPOCHS: i64 = 200; // 迭代次数
const THRES: f64 = 0.001; // 阈值

let mut loss_diff;
let mut curr_loss = 0.0;

// 开始训练
'train: for epoch in 1..N_EPOCHS{
    // neural network multiplication
    let logits = train_data.matmul(&ws) + &bs; 
    // 用log softmax计算loss
    let loss = logits.log_softmax(-1, Kind::Float).nll_loss(&train_lbl);
    // 处理梯度
    ws.zero_grad();
    bs.zero_grad();
    loss.backward();
    // 反向传播
    no_grad(|| {
        ws += ws.grad()*(-1);
        bs += bs.grad()*(-1);
    });
    // 验证
    let val_logits = val_data.matmul(&ws) + &bs;
    let val_accuracy = val_logits
            .argmax(Some(-1), false)
            .eq_tensor(&val_lbl)
            .to_kind(Kind::Float)
            .mean(Kind::Float)
            .double_value(&[]);

    println!(
            "epoch: {:4} train loss: {:8.5} val acc: {:5.2}%",
            epoch,
            loss.double_value(&[]),
            100. * val_accuracy
    );
    // 判断是否达到精度要求 
    if epoch == 1{
        curr_loss = loss.double_value(&[]);
    } else {
        loss_diff = (loss.double_value(&[]) - curr_loss).abs(); 
        curr_loss = loss.double_value(&[]); 
        // 如果loss小于阈值则停止循环
        if loss_diff < THRES {
            println!("Target accuracy reached, early stopping");
            break 'train;
        }
    }
} 

// 在测试集上测试模型效果
let test_logits = test_data.matmul(&ws) + &bs; 
let test_accuracy = test_logits
        .argmax(Some(-1), false)
        .eq_tensor(&test_lbl)
        .to_kind(Kind::Float)
        .mean(Kind::Float)
        .double_value(&[]);
println!("Final test accuracy {:5.2}%", 100.*test_accuracy);

上面代码主体逻辑是一个循环,我们将其命名为'train。循环中我们监控每次迭代的loss,如果连续两次循环的loss差小于给定阈值THRES则结束循环(这里的处理不一定合理,但是为了演示简单起见,我们暂且这样处理)。整体逻辑非常简单,就是最最简单的神经网络,相信大家都能理解其逻辑,我这里不做过多的赘述。

我们执行上面代码即可训练模型,由于模型简单,在我的笔记本上大约十几秒即可训练完成,最终准确率90.45%。

在这里插入图片描述

用tch-rs搭建序贯神经网络

我们再来看一下序贯神经网络的实现。

首先,我们需要引入tch::nn::Module,然后实现fn net(vs: &nn::Path) -> impl Module函数。该函数接收nn::Path输入参数,表示运行神经网络的硬件信息(例如CPU还是GPU),返回一个Module实现。

use tch::{kind, Kind, Tensor, nn, nn::Module, nn::OptimizerConfig, Device};

const IMAGE_DIM: i64 = 784;
const HIDDEN_NODES: i64 = 128;

fn net(vs: &nn::Path) -> impl Module{
    nn::seq()
    .add(nn::linear(vs/"layer1", IMAGE_DIM, HIDDEN_NODES, Default::default() ))
    .add_fn(|xs| xs.relu())
    .add(nn::linear(vs, HIDDEN_NODES, LABELS, Default::default()))
}

接着我们通过如下代码创建神经网络:

// 创建变量保存CUDA是否可用
let vs = nn::VarStore::new(Device::cuda_if_available());
// 创建序贯网络
let net = net(&vs.root());
// 创建优化器
let mut opt = nn::Adam::default().build(&vs, 1e-4)?;

这里我们使用Adam优化器。然后,我们可以简单地按照PyTorch的步骤进行操作,我们需要多轮迭代,并使用优化器的backward_step方法执行反向传播,代码如下:

for epoch in 1..N_EPOCHS {
        let loss = net.forward(&train_data).cross_entropy_for_logits(&train_lbl);
        // 反向传播 
        opt.backward_step(&loss);
        // 计算测试集上的精度
        let val_accuracy = net.forward(&val_data).accuracy_for_logits(&val_lbl);
        println!(
            "epoch: {:4} train loss: {:8.5} val acc: {:5.2}%",
            epoch,
            f64::from(&loss),
            100. * f64::from(&val_accuracy),
        );
    }

经过大约1分钟的训练,最终模型准确率85.50%

在这里插入图片描述

用tch-rs搭建卷积神经网络

我们日常用的最多的神经网络当属卷积神经网络,文章最后我们看一下如何用tch-rs实现卷积神经网络。

首先我们需要先引入nn::ModuleT,该模块特性是一个附加的训练参数,通常用于区分训练和评估之间的网络行为。然后,我们定义结构体Net,它由两个conv2d层和两个线性层组成。

use tch::{kind, Kind, Tensor, nn, nn::ModuleT, nn::OptimizerConfig, Device};

#[derive(Debug)]
struct Net {
    conv1: nn::Conv2D,
    conv2: nn::Conv2D,
    fc1: nn::Linear,
    fc2: nn::Linear,
}

Net结构的实现定义了网络如何构成。两个卷积层的步长(Stride)分别为1和32,填充(Padding)分别为32和64,扩张(Dilation )分别为5和5。线性层接收1024个输入,最终层返回10个元素的输出。

impl Net {
    fn new(vs: &nn::Path) -> Net {
        let conv1 = nn::conv2d(vs, 1, 32, 5, Default::default());
        let conv2 = nn::conv2d(vs, 32, 64, 5, Default::default());
        let fc1 = nn::linear(vs, 1024, 1024, Default::default());
        let fc2 = nn::linear(vs, 1024, 10, Default::default());
        Net { conv1, conv2, fc1, fc2 }
    }
}

最后,我们要实现NetModuleT模块特性。这里前向步骤forward_t接收一个额外的布尔参数train表示是否为训练集,返回一个Tensor张量。前向步骤会用到卷积层以及max_pool_2ddropout。dropout仅用于训练目的,因此要传入布尔变量train

impl nn::ModuleT for Net {
    fn forward_t(&self, xs: &Tensor, train: bool) -> Tensor {
        xs.view([-1, 1, 28, 28])
            .apply(&self.conv1)
            .max_pool2d_default(2)
            .apply(&self.conv2)
            .max_pool2d_default(2)
            .view([-1, 1024])
            .apply(&self.fc1)
            .relu()
            .dropout(0.5, train)
            .apply(&self.fc2)
    }
}

为了提高训练性能,我们使用张量批处理来训练卷积层。为此,我们额外实现一个函数generate_random_index,将输入张量随机拆分为指定大小的批次:

const BATCH_SIZE: i64 = 256;

pub fn generate_random_index(ArraySize: i64, BatchSize: i64)-> Tensor{
    let random_idxs = Tensor::randint(ArraySize, &[BatchSize], kind::INT64_CPU);
    random_idxs
}

训练过程依然是一个循环迭代。输入数据被拆分为n_it个批次,对每一批数据我们通过网络计算loss并用backward_step反向传播误差。代码如下:

let n_it = (TRAIN_SIZE as i64) / BATCH_SIZE;

for epoch in 1..N_EPOCHS {
        // generate random idxs for batch size 
        // run all the images divided in batches  -> for loop
        for i in 1..n_it {
            let batch_idxs = generate_random_index(TRAIN_SIZE as i64, BATCH_SIZE); 
            let batch_images = train_data.index_select(0, &batch_idxs).to_device(vs.device()).to_kind(Kind::Float); 
            let batch_lbls = train_lbl.index_select(0, &batch_idxs).to_device(vs.device()).to_kind(Kind::Int64);
            // compute the loss 
            let loss = net.forward_t(&batch_images, true).cross_entropy_for_logits(&batch_lbls);
            opt.backward_step(&loss);
        }
        // compute accuracy 
        let val_accuracy =
            net.batch_accuracy_for_logits(&val_data, &val_lbl, vs.device(), 1024);
        println!("epoch: {:4} test acc: {:5.2}%", epoch, 100. * val_accuracy,);
    }

在我的笔记本电脑上运行卷积网络需要几分钟,验证准确率达到97.40%。

在这里插入图片描述

总结

整体上tch-rs的使用思路和PyTorch是一致的,因为本身tch-rs就是PyTorch的C++库libtorch的绑定。如果你熟练使用PyTorch,那么用tch-rs上手会非常快。关键是用tch-rs能够带给你更快的速度,这在大规模项目中是一个巨大的优势。

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

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

相关文章

autodeauth:一款功能强大的自动化Deauth渗透测试工具

关于autodeauth autodeauth是一款功能强大的自动化Deauth渗透测试工具&#xff0c;该工具可以帮助广大研究人员以自动化的形式针对本地网络执行Deauth渗透测试&#xff0c;或者枚举公共网络。当前版本的autodeauth已在树莓派OS和Kali Linux平台上进行过测试&#xff0c;之后的…

Presidential靶机总结

Presidential靶机渗透总结 靶机下载地址: https://download.vulnhub.com/presidential/Presidential.ova 打开靶机,使用nmap扫描出靶机的ip和所有开放的端口 可以看到靶机开放了80端口和2082端口 使用-sV参数查看详细服务 80端口是http服务 2082端口是ssh服务 那么我们先根据…

双向链表实现简单的增删查改

前言&#xff1a;上次分享了单向链表的增删查改&#xff0c;这次要介绍双向链表的增删查改&#xff0c;其实双向链表也有多种&#xff0c;这次主要介绍结构最复杂但是实现起功能反而最简单的带头双向循环链表&#xff0c;希望我的分享对各位有些许帮助。学习这篇文章的内容最好…

[虾说IT]GIS与三高架构(一)什么是高性能

大家好&#xff0c;我是消失了一个年假的不愿意透露姓名的神秘虾神&#xff0c;这是癸卯兔年虾神的第一个系列&#xff0c;聊聊GIS中的架构设计&#xff0c;不过你如果是做其他架构的也差不多……总之是架构是虾神的本职工作之一&#xff0c;那么培养更多的架构设计者和爱好者&…

基于前馈补偿的PID控制算法及仿真

在高精度伺服控制中&#xff0c;前馈控制可用来提高系统的跟踪性能。经典控制理论中的前馈控制设计是基于复合控制思想&#xff0c;当闭环系统为连续系统时&#xff0c;使前馈环节与闭环系统的传递函数之积为1&#xff0c;从而实现输出完全复现输入。利用前馈控制的思想&#x…

剑指 Offer 05. 替换空格 [C语言]

目录题目思路1代码1结果1思路2代码2结果2该文章只是用于记录考研复试刷题题目 请实现一个函数&#xff0c;把字符串 s 中的每个空格替换成"%20"。 示例 1&#xff1a; 输入&#xff1a;s “We are happy.” 输出&#xff1a;“We%20are%20happy.” 限制&#xff…

pnpm 简介

本文引用自 摸鱼wiki 1. 与npm&#xff0c;yarn性能比较 actioncachelockfilenode_modulesnpmpnpmYarnYarn PnPinstall33.8s20.1s20.3s40.7sinstall✔✔✔2.1s1.4s2.6sn/ainstall✔✔9.1s5.3s7.8s1.7sinstall✔13.5s9.3s14.1s7.7sinstall✔15s17.2s14.2s33.4sinstall✔✔2.5s3s…

2.JSX

JSX(JavaScript XML) 是 JavaScript 的语法扩展&#xff0c;格式上比较像模板语言。React支持JSX 下面两个代码可以实现相同的功能&#xff0c;JSX看起来要简洁一些 目录 1 使用环境 2 React中的JSX 2.1 特殊的属性 2.2 没有子节点的标签 2.3 小括号包裹 3 JSX使用…

vue 实现动态路由

vue-router对象中的addRoutes&#xff0c;用它来动态添加路由配置格式&#xff1a;router.addRoutes([路由配置对象]) this.$router.addRoutes([路由配置对象])举个例子&#xff1a;// 按钮 <button click"hAddRoute">addRoute</button>// 回调 hAddRout…

感染了恶意软件怎么办?

近日&#xff0c;研究人员披露了一种恶意软件&#xff0c;这种恶意软件已经感染了一系列广泛的 Linux 和 Windows 设备。恶意软件攻击事件的频繁发生&#xff0c;除了黑客的恶意攻击外&#xff0c;还有企业内部自身的问题&#xff0c;下面列举了7种容易感染恶意软件的途径和解决…

2023年2月软考高级-信息系统项目管理师【报名入口】

信息系统项目管理师是全国计算机技术与软件专业技术资格&#xff08;水平&#xff09;考试&#xff08;简称软考&#xff09;项目之一&#xff0c;是由国家人力资源和社会保障部、工业和信息化部共同组织的国家级考试&#xff0c;既属于国家职业资格考试&#xff0c;又是职称资…

coresight(六) power requestor

power requestor power requestor属于coresight组件。这个组件用来控制系统的power domain&#xff0c;最多可以控制32个。 如果没有power requestor&#xff0c;通过DAP&#xff0c;只能对整个coresight系统进行上下电操作&#xff0c;但是有了power requestor&#xff0c;可…

2Pai半导体-推出π122E61双通道数字隔离器 智能分压技术 兼容代替Si8622ET-IS

2Pai半导体-推出π122E61双通道数字隔离器 智能分压技术 兼容代替Si8622ET-IS 电路简单、稳定性更高 &#xff0c;具有出色的性能特征和可靠性&#xff0c;整体性能优于光耦和基于其他原理的数字隔离器产品。 产品传输通道间彼此独立&#xff0c;可实现多种传输方向的配置&…

开源工作流可以解决什么问题?

要了解这个问题&#xff0c;就需要先弄清楚相关概念。为什么要使用开源工作流&#xff0c;可以解决什么问题&#xff1f;如果要实现某个业务目标&#xff0c;提高办公协作效率&#xff0c;就可以用开源工作流在多个参与者之间&#xff0c;借助计算机&#xff0c;按照某种预定规…

Oracle重写sql经典50题

Oracle重写sql经典50题oracle与mysql还是有区别的表的数据只能一条一条的插日期的插入不能想mysql一样直接插&#xff0c;得转换格式mysql里的ifnull&#xff0c;oracle里没有这个函数&#xff0c;用nvl代替mysql里的limit在oracle里也没有&#xff0c;要用rownum查询&#xff…

力扣 76. 最小覆盖子串

一、题目 二、 示例 三、提示 四、 思路与代码实现 1. 思路 本题&#xff0c; 套用的是滑动窗口算法模板;初始化左右窗口边界指针&#xff08;要方便源串取值&#xff09; left 0, right 0&#xff0c; 为什么这样初始化&#xff1f; 若设置窗口索引为左闭右闭区间&#xf…

英语学习打卡day8

2023.1.29 1. affluent adj.富裕的&#xff0c;富足的&#xff0c;流畅的n.支流&#xff0c;富人 flu交通流动、发达-流畅的 affluent society affluent neighborhood 2.conception 概念&#xff0c;观念;受孕&#xff0c;怀孕 conceive v.构思&#xff0c;设想;使受孕&…

【Redis | 黑马点评】短信登陆

文章目录项目概述项目前置准备短信登陆基于Session实现登录流程实现发送短信验证码功能实现短信验证码登录和注册功能实现登录校验拦截器隐藏用户敏感信息集群的Session共享问题基于Redis实现共享Session登录登录拦截器的优化项目概述 短信登录 这一块我们会使用redis共享sess…

ExecutorService线程池

文章目录ExecutorService线程池1 ExecutorService API 介绍1.1 api1.1.1 awaitTermination 方法1.1.2 invokeAll 方法1.1.3 invokeAny方法1.1.4 shutdown 方法1.1.5 shutdownNow方法1.1.6 isShutdown方法1.1.7 submit方法1.1.8 isTerminated方法ExecutorService线程池 1 Execu…

Makefile学习笔记(一)

背景 最近在看ATF代码的时候&#xff0c;想要编译下&#xff0c;实施起来遇到一些问题&#xff0c;其中makefile有些命令&#xff0c;语法不是很清晰&#xff0c;故希望重新系统学习下。学习主要参考跟我一起写Makefile-陈皓.pdf。 第一部分、概述 makefile解决的问题&#…