rust机器学习之tch-rs(代码片段)

JarodYv JarodYv     2023-03-29     428

关键词:

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

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

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

tch-rs是PyTorch接口的Rust绑定,可以认为tch-rs是Rust版的PyTorch。tch-rsLaurent 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查看详情  

机器学习之特征选择方法(代码片段)

特征选择是一个重要的数据预处理过程,在现实机器学习任务中,获得数据之后通常先进行特征选择,此后在训练学习器,如下图所示:进行特征选择有两个很重要的原因:避免维数灾难:能剔除不相关(irrelevant)或冗余(redundant)... 查看详情

浅谈机器学习之深度学习(代码片段)

...资料,学习建议1.1.4深度学习之“深度”深度学习是机器学习的一个分支领域:它是从数据中学习表示的一种新方法,强调从连续的层(layer)中进行学习,这些层对应于越来越有意义的表示。“深度学习... 查看详情

prometheus学习之机器监控(代码片段)

Prometheus使用exporter工具来暴露主机和应用程序上的指标。今天我们就使用node_exporter来收集各种主机指标数据(如:CPU、内存和磁盘等)。安装node_exporter从 Prometheus 的官网下载安装包,这里下载的是 Linux安装... 查看详情

prometheus学习之机器监控(代码片段)

Prometheus使用exporter工具来暴露主机和应用程序上的指标。今天我们就使用node_exporter来收集各种主机指标数据(如:CPU、内存和磁盘等)。安装node_exporter从 Prometheus 的官网下载安装包,这里下载的是 Linux安装... 查看详情

机器学习之推荐算法(代码片段)

1、知识点"""推荐系统1、相似度计算:1、欧几里德距离2、皮尔逊相关系数3、Cosin距离2、推荐相似度选择:1、固定数量的邻居2、基于相似度门槛的邻居3、基于用户的协同过滤:根据用户和其他用户之间的相关系数值,选择值越... 查看详情

机器学习之深度学习入门(代码片段)

...习资料,学习建议本文用浅显易懂的语言精准概括了机器学习的相关知识,内容全面,总结到位,剖析了机器学习的what,who,when,where,how,以及why等相关问题。从机器学习的概念,到机器学习的发... 查看详情

机器学习之集成学习算法(代码片段)

...#xff0c;因此优于任何一个单分类的做出预测。1.2复习:机器学习的两个核⼼任务任务一:如何优化训练数据 —>主要用于解决欠拟合问题任务二:如何提升泛化性能 — 查看详情

机器学习之线性回归(代码片段)

文章目录评价方法一元线性回归np.polyfit求解带入公式求解化简公式求解lstsq求解多元线性回归代码实现一元多项式回归代码实现参考评价方法回归问题有很多的评价方法。这里主要想写一下R^2的计算方法。需要计算R^2需要先弄清... 查看详情

机器学习之支持向量机(手推公式版)(代码片段)

...ine,SVM)源于统计学习理论,是一种二分类模型,是机器学习中获得关注最多的算法,没错, 查看详情

机器学习之linearregression线性回归(代码片段)

一、预测先来看看这样一个场景:假如你手头有一套房子要出售,你咨询了房产中介。中介跟你要了一系列的数据,例如房子面积、位置、楼层、年限等,然后进行一系列计算后,给出了建议的定价。房产中介是如何帮你定价的... 查看详情

集成学习之随机森林案例专题python机器学习系列(十七)(代码片段)

集成学习之随机森林案例专题【Python机器学习系列(十七)】文章目录1.Bagging与随机森林简介2.随机森林--分类任务2.1准备数据2.2python实现随机森林--分类任务2.3绘制ROC曲线与计算AUC2.4绘制决策树3.随机森林--回归任务集成... 查看详情

机器学习之分类模型评估指标及sklearn代码实现(代码片段)

文章目录前言Accuracy(准确率)、Recall(召回率)、Precision(精确率)、F1评分(F1-Score)Accuracy(准确率)Recall(召回率)Precision(精确率)F1评 查看详情

强烈推荐机器学习之算法篇(代码片段)

机器学习算法机器学习算法数据类型:可用数据集:监督学习和无监督学习:算法分类:scikit-learn数据集获取数据集:获取数据集方式:数据集的划分:本地数据集:分类数据集:回归数据集&#x... 查看详情

强烈推荐机器学习之算法篇(代码片段)

机器学习算法机器学习算法数据类型:可用数据集:监督学习和无监督学习:算法分类:scikit-learn数据集获取数据集:获取数据集方式:数据集的划分:本地数据集:分类数据集:回归数据集&#x... 查看详情

机器学习之tensorflow-补充学习中20220821(代码片段)

文章目录前言一、前置基础1.1什么是神经网络1.2什么是线性回归1.3示例:识别手写数字1.4示例:图像识别分类1.5待定番外Java/Python业务通信二、待定总结前言以下内容是在学习过程中的一些笔记,难免会有错误和纰漏... 查看详情

机器学习之k-近邻(knn)算法(代码片段)

 一.K-近邻算法(KNN)概述   最简单最初级的分类器是将全部的训练数据所对应的类别都记录下来,当测试对象的属性和某个训练对象的属性完全匹配时,便可以对其进行分类。但是怎么可能所有测试对象都会找到... 查看详情

菜鸟之路——机器学习之knn算法个人理解及python实现(代码片段)

KNN(KNearestNeighbor)还是先记几个关键公式距离:一般用Euclideandistance  E(x,y)√∑(xi-yi)2 。名字这么高大上,就是初中学的两点间的距离嘛。     还有其他距离的衡量公式,余弦值(cos),相关度(corr... 查看详情