深度学习 - TensorFlow Lite模型,云侧训练与安卓端侧推理

2023-05-16

TensorFlow Lite模型,云侧训练与安卓端侧推理

  • 引言
  • 一、云侧深度模型的训练代码
    • 1.加载数据集的格式分析
      • 1.1 从数据集加载的数据格式
      • 1.2 对加载的数据进行处理
    • 2. 深度模型搭建
    • 3. 模型训练、评估、保存、转换
    • 4. 模型预测
  • 二、端侧安卓的推理代码
    • 1. 安卓项目配置
      • 1.1 app.gradle引入依赖
      • 1.2 AndroidManifest.xml新增照相机权限
      • 1.3 模型放置
    • 2. 安卓端侧代码实现
      • 2.1 布局文件
      • 2.2 主函数文件
      • 2.3 mnist数据集工具类
  • 三、测试结果
  • 参考网址
  • 总结

引言

本次博客主要基于TensorFlow官网的demo进行学习,把学习过程的心得理解记录。其主要内容为TensorFlow云侧训练深度模型,并转换为手机端lite深度模型,最后在安卓手机端侧利用该模型进行推理得出预测结果。本次学习以mnist数据集为例,毕竟入手深度学习,mnist相当于学习编程语言的Hello World!利用的工具有Anaconda的Jupyter Notebook,和Android Studio。

一、云侧深度模型的训练代码

1.加载数据集的格式分析

import tensorflow as tf
import numpy as np
import os
import matplotlib.pyplot as plt

class MNISTLoader():
    def __init__(self):
        mnist = tf.keras.datasets.mnist
        (self.train_data, self.train_label), (self.test_data, self.test_label) = mnist.load_data()

        # MNIST中的圖片預設為uint8(0-255的數字)。以下程式碼將其正規化到0-1之間的浮點數,並在最後增加一維作為顏色通道
        self.train_data = np.expand_dims(self.train_data.astype(np.float32) / 255.0, axis=-1)      # [60000, 28, 28, 1]
        self.test_data = np.expand_dims(self.test_data.astype(np.float32) / 255.0, axis=-1)        # [10000, 28, 28, 1]
        self.train_label = self.train_label.astype(np.int32)    # [60000]
        self.test_label = self.test_label.astype(np.int32)      # [10000]
        self.num_train_data, self.num_test_data = self.train_data.shape[0], self.test_data.shape[0]

导入TensorFlow和numpy包即可,我们会用到TensorFlow的Keras,它是用 Python 编写的高级神经网络 API,支持快速的构建网络框架。

1.1 从数据集加载的数据格式

先对MNISTLoader这个类进行分析,该类先加载了数据集数据,如下。

(train_data, train_label), (test_data, test_label) = mnist.load_data()

打印数据格式如下。

print("train_data:变量类型={0},变量形状={1},数据类型={2}".format(type(train_data), train_data.shape, train_data.dtype))
print("train_label:变量类型={0},变量形状={1},数据类型={2}".format(type(train_label), train_label.shape,train_label.dtype))
print("test_data:变量类型={0},变量形状={1},数据类型={2}".format(type(test_data), test_data.shape,test_data.dtype))
print("test_label:变量类型={0},变量形状={1},数据类型={2}".format(type(test_label), test_label.shape,test_label.dtype))

打印结果如下。

train_data:变量类型=<class 'numpy.ndarray'>,变量形状=(60000, 28, 28),数据类型=uint8
train_label:变量类型=<class 'numpy.ndarray'>,变量形状=(60000,),数据类型=uint8
test_data:变量类型=<class 'numpy.ndarray'>,变量形状=(10000, 28, 28),数据类型=uint8
test_label:变量类型=<class 'numpy.ndarray'>,变量形状=(10000,),数据类型=uint8

也就是说加载了60000张28×28的图片作为训练集,10000张28×28的图片作为测试集。其中的数据类型为uint8,取值为0~255。
接着又用了np.expand_dims()为图片的数据集进行了维度扩展,axis=-1表示在原来的变量形状的最后一个维度增加多一维,-1在python的索引通常都是表示最后一个索引。为什么要增加这么个维度呢?因为最后一个维度的数值表示图片的通道数。比如图片为RGB图时,最后一个维度的数值是3,而mnist的数据集为灰度图片,即单通道表示的图片,所以最后一个维度数值是1。train_label、test_label的数据则是用0~9表示对应数据集的各个类。

1.2 对加载的数据进行处理

对加载的数据进行的运算,主要包括对图片进行0~1数值的归一化,维度扩展,和数据类型转换;对标签值进行数值类型转换。注意对数值类型转换尤为重要,这跟后续在安卓端编程中需要用到什么数据类型来作为输入输出要对应起来。数据转换的语句如下。

train_data = np.expand_dims(train_data.astype(np.float32) / 255.0, axis=-1)
train_label = train_label.astype(np.int32)

再次运行如下语句查看数据格式

print("train_data:变量类型={0},变量形状={1},数据类型={2}".format(type(train_data), train_data.shape, train_data.dtype))
print("train_label:变量类型={0},变量形状={1},数据类型={2}".format(type(train_label), train_label.shape, train_label.dtype))

得到了新的数据格式,作为最终输入到模型进行训练的数据格式

train_data:变量类型=<class 'numpy.ndarray'>,变量形状=(60000, 28, 28, 1),数据类型=float32
train_label:变量类型=<class 'numpy.ndarray'>,变量形状=(60000,),数据类型=int32

2. 深度模型搭建

用Keras的Sequential来按顺序搭建模型,超级简单。需要添加的神经网络层,只需要add进来就可以了,Keras提供了很多常用的网络层。同时目前最新版本的Keras搭建模型时,每一层(包括首层输入层)的输入会根据上一层的输出自动推断,所以不需要input_shape参数。

model = tf.keras.models.Sequential()
model.add(tf.keras.layers.Conv2D(
    filters=32,             # 卷积滤波器数量
    kernel_size=[5, 5],     # 卷积核大小
    padding="same",         # padding策略
    activation=tf.nn.relu   # 激活函数
))
model.add(tf.keras.layers.MaxPool2D(pool_size=[2, 2], strides=2))
model.add(tf.keras.layers.Conv2D(
    filters=64,
    kernel_size=[5, 5],
    padding="same",
    activation=tf.nn.relu
))
model.add(tf.keras.layers.MaxPool2D(pool_size=[2, 2], strides=2))
model.add(tf.keras.layers.Reshape(target_shape=(7 * 7 * 64,)))
model.add(tf.keras.layers.Dense(units=1024, activation=tf.nn.relu))
model.add(tf.keras.layers.Dense(units=10, activation=tf.nn.softmax))

深度模型

3. 模型训练、评估、保存、转换

num_epochs = 20
batch_size = 50
learning_rate = 0.001
save_path = r"D:\code\jupyter\saved"

# 数据加载器
data_loader = MNISTLoader()

# 模型编译
model.compile(
    optimizer=tf.keras.optimizers.Adam(learning_rate=learning_rate),
    loss=tf.keras.losses.sparse_categorical_crossentropy,
    metrics=[tf.keras.metrics.sparse_categorical_accuracy]
)

# 模型训练
model.fit(data_loader.train_data, data_loader.train_label,
          epochs=num_epochs, batch_size=batch_size)

# 模型评估
print(model.evaluate(data_loader.test_data, data_loader.test_label))

# 模型保存
model.save(save_path)

# 模型转换
converter = tf.lite.TFLiteConverter.from_saved_model(save_path)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
tflite_quant_model = converter.convert()
open(os.path.join(save_path, "mnist_savedmodel_quantized.tflite"),
     "wb").write(tflite_quant_model)

模型的损失函数采用了sparse_categorical_crossentropy,则不同类的label直接用数字表示就可以了,如数字2的图片对应的label值为2。

模型训练时会动态给出结果如下:

1200/1200 [==============================] - 42s 35ms/step - loss: 0.0249 - sparse_categorical_accuracy: 0.9924

模型评估时会动态给出结果如下:

313/313 [==============================] - 2s 6ms/step - loss: 0.0375 - sparse_categorical_accuracy: 0.9881

最后模型mnist_savedmodel_quantized.tflite保存到了相应的路径save_path,同时,利用转换器转换为适合安卓手机端使用的量化模型。

4. 模型预测

# 找测试数据第一张图片来看看,展示的时候shape是28*28
im = data_loader.test_data[0].reshape(28, 28)
fig = plt.figure()
plotwindow = fig.add_subplot(111)
plt.axis('off')
plt.imshow(im, cmap='gray')
plt.show()
plt.close()

mnist数字7
预测图片如下:

im = im.reshape(1, 28, 28, 1)
print("各个类的概率:{0}".format(model.predict(im)))
print("最大概率的类:{0}".format(model.predict_classes(im))) 

关于模型的输入格式,由于我们在构建model的时候,首层Conv2D没有使用data_format参数,其默认输入格式为channels_last,即batch_shape + (spatial_dim1, spatial_dim2, spatial_dim3, channels)。所以reshape的第一个数字是batch_size,最后一个数字是颜色通道数。

输出结果如下:

各个类的概率:[[9.9865129e-09 4.3024698e-08 5.2642001e-05 3.9080669e-06 2.2962024e-10
  2.2086294e-07 5.7997704e-13 9.9992096e-01 2.2194282e-08 2.2103426e-05]]
最大概率的类:[7]

通过上面的例子可知,我们直接预测的输出是一个包含各个类的预测概率的数组,而通过model.predict_classes(im)则会拿到预测数组里分值最高的数值对应的索引,model.predict_classes() 该方法将会被抛弃,提示使用np.argmax(model.predict(x), axis=-1)

二、端侧安卓的推理代码

安卓端实现通过调用相机获取图片输入,接着通过模型推理后打印日志输出结果。

1. 安卓项目配置

1.1 app.gradle引入依赖

android {
    aaptOptions {
        noCompress "tflite" // 编译apk时,不压缩tflite文件
    }
}
dependencies {
    implementation 'org.tensorflow:tensorflow-lite:2.4.0' // 推理工具
    implementation 'org.tensorflow:tensorflow-lite-support:0.2.0' // 用于读取加载模型
}

1.2 AndroidManifest.xml新增照相机权限

<uses-permission android:name="android.permission.CAMERA" />

1.3 模型放置

把转换后的模型mnist_savedmodel_quantized.tflite放置到src\main\assets目录下,没该目录的需新建一个。

2. 安卓端侧代码实现

2.1 布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical"
    android:gravity="center">

    <ImageView
        android:id="@+id/camera_image"
        android:layout_weight="1"
        android:layout_width="wrap_content"
        android:layout_height="0dp">
    </ImageView>
    <Button
        android:id="@+id/open_camera_button"
        android:text="打开相机"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
    </Button>

</LinearLayout>

2.2 主函数文件

package com.example.tensorflowlite;

import java.io.IOException;

import org.tensorflow.lite.Interpreter;
import org.tensorflow.lite.support.common.FileUtil;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;

/**
 * 主活动页,通过点击底部打开相机按钮,拍照后返回主页,在主页显示照片图像
 * 同时日志打印推理的结果
 */
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private static final String TAG = "MainActivity";

    private static final String MODEL_PATH = "mnist_savedmodel_quantized.tflite";

    private static final int CAMERA_PERMISSION_REQ_CODE = 1;

    private static final int CAMERA_CAPTURE_REQ_CODE = 2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = findViewById(R.id.open_camera_button);
        button.setOnClickListener(this);
    }

    /**
     * 打开照相机
     */
    private void openCamera() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
            // 无权限,引导用户授予权限
            if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
                // 提示已经禁止
                Log.e(TAG, "error");
            } else {
                // 请求相机权限
                ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.CAMERA},
                    CAMERA_PERMISSION_REQ_CODE);
            }
        } else {
            // 有权限,直接打开相机,并等待回调
            Intent camera = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
            startActivityForResult(camera, CAMERA_CAPTURE_REQ_CODE);
        }
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.open_camera_button:
                openCamera();
                break;
            default:
                Log.i(TAG, "nothing");
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK && requestCode == CAMERA_CAPTURE_REQ_CODE) {
            Bundle extras = data.getExtras();

            // 拿到的数据变得很小,被压缩过了,对于mnist数据集来说,够够的了
            Bitmap bitmap = (Bitmap) extras.get("data");

            // 画出拿到的数据
            ImageView cameraImage = findViewById(R.id.camera_image);
            cameraImage.setImageBitmap(bitmap);

            // 推理
            inference(bitmap);
        }
    }

    /**
     * 对图像进行推理
     */
    private void inference(Bitmap bitmap) {
        try {
            // 加载模型后的解释器
            Interpreter interpreter =
                new Interpreter(FileUtil.loadMappedFile(this, MODEL_PATH), new Interpreter.Options());

            // 新建变量,用于存放推理输出结果
            float[][] labelProbArray = new float[1][10];

            // 开始推断
            interpreter.run(MnistUtil.convertBitmapToByteBuffer(bitmap), labelProbArray);

            // 打印推断结果,顺序按
            for (int i = 0; i < labelProbArray[0].length; i++) {
                Log.i(TAG, labelProbArray[0][i] + "");
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在主活动页中,通过点击底部打开相机按钮,拍照后返回主页,在主页显示照片图像同时日志打印推理的结果。主要的函数有:openCamera()打开相机,onActivityResult(int requestCode, int resultCode, @Nullable Intent data)等待相机回调结果获取图片,inference(Bitmap bitmap)对图像进行推理,同时显示图像和打印推理结果。

2.3 mnist数据集工具类

package com.example.tensorflowlite;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

import android.graphics.Bitmap;

/**
 * mnist数据集工具
 */
public class MnistUtil {
    public static ByteBuffer convertBitmapToByteBuffer(Bitmap bitmap) {
        // 定义图像的宽高
        int dimImgWidth = 28;
        int dimImgHeight = 28;

        // 推理时,一次只推理一张图像
        int dimBatchSize = 1;

        // 相当于云侧训练时用np.expand_dims多扩展出来的一维
        int dimPixelSize = 1;

        // 一个float等于4个字节
        int numBytesPerChannel = 4;

        // 存放图像数据的数组
        int[] intValues = new int[dimImgWidth * dimImgHeight];

        // 缩放图像至 28 * 28
        Bitmap scaleBitmap = Bitmap.createScaledBitmap(bitmap, dimImgWidth, dimImgHeight, true);

        // 复制缩放后的bitmap到存放图像数据的数组
        scaleBitmap.getPixels(intValues, 0, scaleBitmap.getWidth(), 0, 0, scaleBitmap.getWidth(),
            scaleBitmap.getHeight());

        // 创建图像数据缓冲区
        ByteBuffer imgData =
            ByteBuffer.allocateDirect(numBytesPerChannel * dimBatchSize * dimImgWidth * dimImgHeight * dimPixelSize);

        // ByteBuffer的字节序设置为当前硬件平台的字节序
        imgData.order(ByteOrder.nativeOrder());

        // 把position设为0,limit不变,一般在把数据重写入Buffer前调用。
        imgData.rewind();

        // 处理图像数据,归一化为0~1的浮点型数据,并把存放图像数据的数组里的数组往缓冲器拷贝
        int pixel = 0;
        for (int i = 0; i < dimImgWidth; ++i) {
            for (int j = 0; j < dimImgHeight; ++j) {
                int val = intValues[pixel++];

                // 添加把Pixel数值转化并添加到ByteBuffer
                addImgValue(imgData, val);
            }
        }
        return imgData;
    }

    /**
     * 添加图像数据值。对图像数据进行处理,归一化至0~1.0的浮点数据
     *
     * @param imgData 缓冲区数据
     * @param val 整形数据
     */
    private static void addImgValue(ByteBuffer imgData, int val) {
        int mImageMean = 0;
        float mImageStd = 255.0f;
        imgData.putFloat(((val & 0xFF) - mImageMean) / mImageStd);
    }
}

注意这里的图像缓冲区大小为什么要乘以4:ByteBuffer.allocateDirect(numBytesPerChannel * dimBatchSize * dimImgWidth * dimImgHeight * dimPixelSize)创建了一个4×1×28×28×1大小的缓冲区存储图片,因为缓冲区是以字节byte来存储的,通过计算,每个图像像素点最终转化为float型,而float型在java虚拟机中以4个字节存在,所以需要乘以4。在图像比较大的时候,缓冲区是很重要的。

三、测试结果

相机界面
日志打印所有类的概率如下:

2021-07-08 10:13:03.325 15543-15543/com.example.tensorflowlite I/MainActivity: 0.0030371095
2021-07-08 10:13:03.325 15543-15543/com.example.tensorflowlite I/MainActivity: 0.003125498
2021-07-08 10:13:03.325 15543-15543/com.example.tensorflowlite I/MainActivity: 0.011447249
2021-07-08 10:13:03.325 15543-15543/com.example.tensorflowlite I/MainActivity: 0.055658735
2021-07-08 10:13:03.325 15543-15543/com.example.tensorflowlite I/MainActivity: 7.467345E-5
2021-07-08 10:13:03.325 15543-15543/com.example.tensorflowlite I/MainActivity: 0.05097304
2021-07-08 10:13:03.325 15543-15543/com.example.tensorflowlite I/MainActivity: 1.911169E-5
2021-07-08 10:13:03.325 15543-15543/com.example.tensorflowlite I/MainActivity: 0.8677362
2021-07-08 10:13:03.326 15543-15543/com.example.tensorflowlite I/MainActivity: 9.3077944E-4
2021-07-08 10:13:03.326 15543-15543/com.example.tensorflowlite I/MainActivity: 0.006997687

结果为0~9按顺序打印后,可以看到数字7的概率为0.8677362。

参考网址

官方安卓端侧代码
官方云侧训练模型代码
Keras中文文档
TensorFlow Lite中文文档

总结

你学会了吗?

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

深度学习 - TensorFlow Lite模型,云侧训练与安卓端侧推理 的相关文章

随机推荐

  • 对于 nested exception is java.net.ConnectException问题【已解决】

    当在springcloud遇到这类问题时 xff0c 需要排查控制器中的路径是否写对 xff0c 由于我报错的项目是一个模拟客户端访问查询业务功能 xff0c 下面是我的源代码 xff08 报错 xff09 xff1a public sta
  • 如何搭建一个基本的Spring项目【Maven】

    相信很多朋友在刚学习Spring时 xff0c 都会存在这个疑问 xff0c 如何正确搭建一个Spring项目 xff0c 以及中间的许多报错无法解决 xff0c 大家可以跟着我一起试一下 xff0c 有问题的朋友评论区见 1 Spring
  • 使用注解开发时需要添加包扫描器【出现问题的可以点进来看看】

    很多第一次接触Spring注解开发的同学 xff0c 在使用注解后容易报错 xff0c 那就很可能是没有在配置文件中加入包扫描器 那么可以往下看 xff0c 以下面这个简单的例子来学习 下面是一个简单的实体类 xff0c 通过Compone
  • 如何使用Autowried,以及与Resource的区别

    在使用注解开发的过程中 xff0c 有个经常会见面的朋友 Autowried xff08 自动装配 xff09 xff0c 刚接触的朋友肯定不能理解自动装配这个词的含义 简单解释一下 xff0c 自动装配将通过已知的类型自动分配对象 xff
  • SpringMVC遇到的有关实体类的报错

    今天在复习SpringMVC数据传参的时候 xff0c 出现了一个非常小的问题 xff0c 主要是为了实验如何将一个对象作为参数传递 xff0c 我创建了两个类 xff0c 但是由于被传递的对象的那个类没有用public 修饰 xff0c
  • app:checkDebugDuplicateClasses错误

    此异常完整表述为 org gradle api tasks TaskExecutionException Execution failed for task 39 app checkDebugDuplicateClasses 39 这类问题
  • LAMP网站架构

    一 LAMP网站架构 1 1 基本定义 LAMP是指一组通常一起使用来运行动态网站或者服务器的自由软件名称首字母缩写 Linux xff0c 操作系统Apache xff0c 网页服务器 MariaDB或MySQL xff0c 数据库管理系
  • pancakeswap薄饼添加流动性后实现永久锁仓

    添加完流动性后 xff0c 永久锁仓就是放弃对资金池的控制 xff0c 就是放弃了对流动性的所有权 xff0c 没有办法撤池子了 现在锁仓分为2种 xff0c 一个是丢黑洞永久锁仓 xff0c 另外一种是短期锁仓 xff0c 锁在智能合约中
  • ubuntu14.04 root用户登录方法

    如果你是刚刚装完ubuntu14 04系统 xff0c 你进去后是以普通用户登录的 xff0c 很多操作并没有权限 xff0c 要想获得全部权限可以以root用户登录 1 先解除root锁定 xff0c 为root用户设置密码 打开终端输入
  • pycharm终端常用指令

    在调试ppddle的时候下载的coco数据集过大 xff0c 一时没有注意不急的如何终止 xff0c 所以转载一个记一下 Terminal快捷键 功能 Tab 自动补全 Ctrl 43 a 光标移动到开始位置 Ctrl 43 e 光标移动到
  • 解决Mac电脑因kotlin插件禁用导致的Android Studio无法打开问题

    解决这个办法需要将Android Studio目录下的disabled plugins txt文档中的org jetbrains kotlin删除即可 文件位置 Users mac Library Application Support G
  • 实现生产者消费者进程(Java)

    目录 前言 一 实验要求 二 步骤 1 主类 2 消费者 3 生产者 4 超市 前言 消费者问题是操作系统中典型的进程同步互斥问题 xff0c xff08 英语 xff1a Producer Consumer problem xff09 x
  • python | Pandas库数据预处理-缺失值篇:info()、isnull()、dropna()、fillna()函数

    相关文章 python Pandas库导入Excel数据 xff08 xlsx格式文件 xff09 函数 xff1a read excel python Pandas库导入csv格式文件函数 xff1a read excel 目录 数据源
  • vue3学习笔记 2023

    vue文件 34 组件 34 是一种封装的思想 把相关业务逻辑的 34 js css html 34 都封装到一起 当需要调用 34 组件 34 的时候 只需要在html中期望的位置插入对应的 34 标签 34 即可 比如封装了一个 34
  • STP详解

    STP STP全称为 生成树协议 xff08 Spanning Tree Protocol xff09 xff0c 是一种网络协议 xff0c 用于在交换机网络中防止网络回路产生 xff0c 保证网络的稳定和可靠性 它通过在网络中选择一条主
  • 【Linux】线程篇---线程安全&&生产者消费者模型

    目录 1 线程安全概念 2 互斥的实现 2 1互斥锁 2 2互斥锁原子性的保证 2 3互斥锁接口 2 3 1初始化互斥锁 2 3 2互斥锁加锁接口 2 3 3解锁接口 2 3 4 销毁互斥锁接口 2 4代码验证锁的接口 3 同步的实现 3
  • 收藏版|史上最全机器学习优化器Optimizer汇总

    转载于收藏版 xff5c 史上最全机器学习优化器Optimizer汇总 掘金 juejin cn 作者 xff1a 苏学算法 链接 xff1a https juejin cn post 7084409806492008456 来源 xff1
  • 三种图像内插法(最近邻内插法、双线性内插法、双三次内插法)的做法 & 代码实现

    参考博客 数字图像处理学习笔记 xff08 四 xff09 数字图像的内插 度量 表示与质量 闭关修炼 暂退的博客 CSDN博客 数字图像处理学习笔记 xff08 七 xff09 用Pycharm及MATLAB实现三种图像内插法 xff08
  • Java实现生产者消费者案例

    目录 一 生产者消费者模式概述 二 生产者消费者案例 三 代码 奶箱类 xff08 Box xff09 生产者类 xff08 Producer xff09 xff1a 消费者类 xff08 Customer xff09 xff1a 测试类
  • 深度学习 - TensorFlow Lite模型,云侧训练与安卓端侧推理

    TensorFlow Lite模型 xff0c 云侧训练与安卓端侧推理 引言一 云侧深度模型的训练代码1 加载数据集的格式分析1 1 从数据集加载的数据格式1 2 对加载的数据进行处理 2 深度模型搭建3 模型训练 评估 保存 转换4 模型