深度学习 · 手撕 DeepLearning4J ,用Java实现手写数字识别 (附UI效果展示)

news2025/1/21 16:17:05

引言

随着人工智能技术的不断发展,手写数字识别已经成为深度学习领域的一个经典案例。不管是老牌的机器学习模型还是现代的神经网络架构,手写数字识别总是大家学习和实战的起点之一。而对于我们日常使用的Java开发者来说,借助DeepLearning4J这个强大的Java深度学习框架,可以很方便地在Java项目中实现手写数字识别的功能。在这里插入图片描述

在本文中,我们会以一种轻松的方式,带你一步步实现一个完整的手写数字识别系统,使用Spring Boot作为后端框架,结合ThymeleafBootstrap来构建用户友好的界面。

为什么选择 DeepLearning4J

在很多人提到深度学习时,首先想到的可能是TensorFlowPyTorch。那么,为什么我们这次要使用DeepLearning4J呢?其实,DeepLearning4J最大的优势就在于它是专门为Java和JVM语言设计的深度学习框架。对于熟悉Java生态的开发者来说,DeepLearning4J让我们可以利用现有的Java工具链和库,轻松构建和部署深度学习模型。

除此之外,DeepLearning4J支持大规模的分布式训练,甚至可以与Hadoop和Spark进行集成,方便企业级应用开发。

项目概览

在本文中,我们将实现一个可以识别手写数字的Web应用。用户可以通过网页上传一张手写数字的图片,系统会自动识别并返回预测结果。

为了实现这个功能,我们会按照以下步骤进行:

  1. 使用DeepLearning4J训练一个简单的神经网络模型,专注于识别手写数字。
  2. 将训练好的模型集成到Spring Boot应用中。
  3. 构建一个Web页面,用户可以上传图片并查看识别结果。

项目结构

我们的项目结构将包括后端服务用于处理上传的图片和执行预测逻辑,以及前端页面用于用户上传图片和查看预测结果。

├── src/
│   ├── main/
│   │   ├── java/
│   │   │   ├── com/neo/
│   │   │   │   ├── controller/
│   │   │   │   │   └── OCRController.java
│   │   │   │   ├── service/
│   │   │   │   │   ├── ImageProcessingService.java
│   │   │   │   │   └── OCRPredictionService.java
│   │   │   │   └── model/
│   │   │   │       └── OCRModelService.java
│   │   ├── resources/
│   │   │   ├── static/
│   │   │   ├── templates/
│   │   │   │   └── upload.html
│   ├── pom.xml

模型训练

使用 DeepLearning4J 进行手写数字识别模型的训练

在这段代码中,我们使用DeepLearning4J框架训练了一个手写数字识别模型,基于经典的MNIST数据集。下面我们将逐步解释每一部分代码的功能和作用,以便更好地理解整个模型训练的过程。

package com.neo.service;

import org.deeplearning4j.datasets.iterator.impl.MnistDataSetIterator;
import org.deeplearning4j.nn.conf.MultiLayerConfiguration;
import org.deeplearning4j.nn.conf.NeuralNetConfiguration;
import org.deeplearning4j.nn.conf.layers.DenseLayer;
import org.deeplearning4j.nn.conf.layers.OutputLayer;
import org.deeplearning4j.nn.multilayer.MultiLayerNetwork;
import org.deeplearning4j.optimize.listeners.ScoreIterationListener;
import org.deeplearning4j.util.ModelSerializer;
import org.nd4j.linalg.activations.Activation;
import org.nd4j.linalg.dataset.api.iterator.DataSetIterator;
import org.nd4j.linalg.learning.config.Nesterovs;
import org.nd4j.linalg.lossfunctions.LossFunctions;

public class OCRModelService {

    public static void trainModel() throws Exception {
        int batchSize = 128;
        int rngSeed = 123;
        int numEpochs = 1;

        DataSetIterator mnistTrain = new MnistDataSetIterator(batchSize, true, rngSeed);

        MultiLayerConfiguration conf = new NeuralNetConfiguration.Builder()
            .seed(rngSeed)
            .updater(new Nesterovs(0.006, 0.9))
            .l2(1e-4)
            .list()
            .layer(0, new DenseLayer.Builder()
                .nIn(28 * 28)
                .nOut(1000)
                .activation(Activation.RELU)
                .build())
            .layer(1, new OutputLayer.Builder(LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD)
                .activation(Activation.SOFTMAX)
                .nIn(1000)
                .nOut(10)
                .build())
            .build();

        MultiLayerNetwork model = new MultiLayerNetwork(conf);
        model.init();
        model.setListeners(new ScoreIterationListener(100));

        model.fit(mnistTrain, numEpochs);

        // 保存模型
        ModelSerializer.writeModel(model, "ocr-model.zip", true);
    }

    public static void main(String[] args) {
        try {
            // 调用训练方法
            OCRModelService.trainModel();
            System.out.println("模型训练完成并保存为 ocr-model.zip");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

详细介绍模型训练

1. 数据集加载

int batchSize = 128;
int rngSeed = 123;
DataSetIterator mnistTrain = new MnistDataSetIterator(batchSize, true, rngSeed);
  • batchSize:每次训练使用的样本数量。在这里,我们设置为128,这意味着每次从数据集中取出128张图片用于模型训练。
  • rngSeed:随机数种子,保证每次运行时数据的随机性一致,方便调试和复现结果。
  • MnistDataSetIterator:这是DeepLearning4J提供的一个迭代器,用于加载MNIST数据集。它会自动将数据集分批加载到内存中,供模型训练使用。

2. 模型配置

MultiLayerConfiguration conf = new NeuralNetConfiguration.Builder()
    .seed(rngSeed)
    .updater(new Nesterovs(0.006, 0.9))
    .l2(1e-4)
    .list()
    .layer(0, new DenseLayer.Builder()
        .nIn(28 * 28)
        .nOut(1000)
        .activation(Activation.RELU)
        .build())
    .layer(1, new OutputLayer.Builder(LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD)
        .activation(Activation.SOFTMAX)
        .nIn(1000)
        .nOut(10)
        .build())
    .build();
  • NeuralNetConfiguration.Builder:这是一个用于配置神经网络的构建器。我们在这里设置了一些全局参数,如随机种子、优化器和正则化参数。
  • seed(rngSeed):设置随机种子,保证每次训练的结果一致。
  • updater(new Nesterovs(0.006, 0.9)):使用Nesterov加速梯度下降法,学习率为0.006,动量为0.9。
  • l2(1e-4):L2正则化,用于防止过拟合。
  • list():开始构建网络的层次结构。
  • DenseLayer.Builder():第一层是一个全连接层,输入为28x28个像素(MNIST图片的尺寸),输出为1000个神经元,使用ReLU激活函数。
  • OutputLayer.Builder():输出层是一个具有10个神经元的全连接层,每个神经元对应一个数字类别(0到9),使用Softmax激活函数来输出概率分布,损失函数选择负对数似然(Negative Log Likelihood)。

3. 模型初始化和训练

MultiLayerNetwork model = new MultiLayerNetwork(conf);
model.init();
model.setListeners(new ScoreIterationListener(100));
  • MultiLayerNetwork:使用配置构建一个多层神经网络模型。
  • model.init():初始化模型,准备进行训练。
  • model.setListeners(new ScoreIterationListener(100)):设置一个监听器,每训练100次迭代后输出一次损失函数的值,帮助我们跟踪模型的训练进度。

4. 训练模型

model.fit(mnistTrain, numEpochs);
  • fit方法接受数据集迭代器和训练轮数(numEpochs)作为参数,进行模型训练。在这段代码中,numEpochs设置为1,这意味着我们只训练一个完整的训练集。

5. 保存模型

ModelSerializer.writeModel(model, "ocr-model.zip", true);
  • ModelSerializer.writeModel:这是一个方便的方法,可以将训练好的模型保存到文件中。我们将模型保存为ocr-model.zip,以便后续加载和使用。

通过这段代码,我们实现了一个简单的深度学习模型,用于手写数字识别。我们详细讲解了每个部分的功能,包括数据加载、模型配置、训练和保存。

白话模型训练

如果说,上面的概念太难懂,那我们来把深度学习模型训练的过程用一个做菜的例子来解释,让它变得更加简单易懂。

假设我们要做一道菜——“手写数字识别”。在这个比喻里,我们的目标就是“做出一道完美的菜”,也就是训练出一个能够准确识别手写数字的模型。而我们做菜的过程就像是模型的训练过程。

1. 准备食材(加载数据集)

在做菜之前,首先要准备好食材。对于我们的“手写数字识别”任务,食材就是MNIST数据集,这个数据集包含了大量的手写数字图片,类似于我们准备的“原料”。

int batchSize = 128;  // 这里就像是一次做菜时需要的食材数量
DataSetIterator mnistTrain = new MnistDataSetIterator(batchSize, true, 123);  // 这是我们的食材(训练数据)
  • **食材准备好了吗?就是数据加载到内存中的过程。我们用MnistDataSetIterator**来把数据集分成一批一批(比如每次做菜时要准备128个食材),确保我们每次只用一定量的食材,避免浪费资源。

2. 挑选菜谱(设置模型配置)

接下来,就像做菜要挑选合适的菜谱一样,我们需要为模型挑选合适的结构和步骤。比如,你要做一道麻辣火锅,你不能随便照着糖醋排骨的菜谱做,它的“配料”和“步骤”都不一样。

MultiLayerConfiguration conf = new NeuralNetConfiguration.Builder()
    .seed(123)  // 随机种子,就像菜谱的版本,确保每次做出来的菜口感一致
    .updater(new Nesterovs(0.006, 0.9))  // 就像选择了一个合适的调味料:Nesterovs优化器
    .l2(1e-4)  // 加一点调味料:L2正则化,防止做出来的菜太腻
    .list()  // 开始选择菜谱中的每个步骤
    .layer(0, new DenseLayer.Builder()
        .nIn(28 * 28)  // 食材的数量,就是手写数字图片的大小
        .nOut(1000)  // 菜谱第一步:做成1000种口味的菜
        .activation(Activation.RELU)  // 用的调味料是ReLU
        .build())
    .layer(1, new OutputLayer.Builder(LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD)
        .activation(Activation.SOFTMAX)  // 做的最后一道工序,用SOFTMAX调料,让它有个概率结果
        .nIn(1000)
        .nOut(10)  // 最后把1000种口味缩减到10种:0到9的数字
        .build())
    .build();

在这里,我们通过**NeuralNetConfiguration.Builder**来搭建了一个神经网络模型,它就像是我们选择的菜谱。我们定义了神经网络的“结构”:

  • 第一层是一个DenseLayer,就是把28x28个像素的图片“切割成”1000个特征,类似于我们把食材切成小块。
  • 第二层是OutputLayer,最后输出10个数字的概率,类似于最后一道步骤:做成一道美味的菜,看看它的口味是哪个。

3. 开始做菜(训练模型)

当你选择好了菜谱,就要开始实际动手做菜了。我们通过训练模型,类似于在厨房里开始切菜、炒菜,经过一段时间的“烹饪”,最终做出一道合格的菜。

MultiLayerNetwork model = new MultiLayerNetwork(conf);  // 按照菜谱开始做菜
model.init();  // 开始做菜的过程
model.setListeners(new ScoreIterationListener(100));  // 观察一下菜的火候,每100次迭代检查一次
model.fit(mnistTrain, 1);  // 做菜:一次完整的“烹饪”,这里是1次迭代
  • 做菜的时间:这里就是模型的训练过程,**model.fit**方法就像是我们把食材按步骤处理、混合,最后做成一道菜。
  • 每100次迭代检查一次菜的火候:这个就像是我们在做菜时,不时检查一下味道(通过**ScoreIterationListener**监听器查看损失函数的变化)。如果菜做得不对,就赶紧调整火候和调料。

4. 做好的菜(保存模型)

一旦菜做得差不多了,我们就要把做好的菜保存下来,下一次可以重新享用,这就像是我们训练好的模型需要保存,以便以后使用。

ModelSerializer.writeModel(model, "ocr-model.zip", true);  // 把做好的菜保存起来,方便下次享用

通过**ModelSerializer.writeModel,我们把做好的模型保存为ocr-model.zip**,它就像是我们做好的菜装进了保鲜盒,可以随时取出来再次使用。

训练和保存模型

model.fit(mnistTrain, numEpochs);
ModelSerializer.writeModel(model, new File("ocr-model.zip"), true);

我们使用model.fit方法进行训练,训练完成后将模型保存到ocr-model.zip文件中。这样,我们的模型就可以在Spring Boot项目中使用了。

图像处理

在Web应用中,用户上传的图片可能是各种格式、尺寸和颜色的。我们需要对图片进行预处理,转换为模型能够接受的输入格式。

package com.neo.service;

import org.nd4j.linalg.api.ndarray.INDArray;
import org.nd4j.linalg.dataset.DataSet;
import org.nd4j.linalg.dataset.api.preprocessor.NormalizerStandardize;
import org.nd4j.linalg.factory.Nd4j;
import org.springframework.stereotype.Service;

import java.awt.image.BufferedImage;

@Service
public class ImageProcessingService {

    public INDArray preprocessImage(BufferedImage image) {
        int width = 28;
        int height = 28;
        INDArray array = Nd4j.zeros(1, width * height);

        // 遍历图像,将每个像素值转换为灰度值
        for (int i = 0; i < width; i++) {
            for (int j = 0; j < height; j++) {
                int rgb = image.getRGB(i, j);
                int r = (rgb >> 16) & 0xFF;
                int g = (rgb >> 8) & 0xFF;
                int b = rgb & 0xFF;
                // 灰度转换公式
                double gray = (0.299 * r + 0.587 * g + 0.114 * b) / 255.0;
                array.putScalar(new int[]{0, j * width + i}, gray);
            }
        }

        // 使用随机数据模拟样本,计算标准化参数
        INDArray sampleData = Nd4j.rand(new int[]{100, width * height});
        DataSet sampleDataSet = new DataSet(sampleData, null);
        NormalizerStandardize normalizer = new NormalizerStandardize();
        normalizer.fit(sampleDataSet); // 计算标准化参数

        // 归一化图像数据
        DataSet dataSet = new DataSet(array, null);
        normalizer.transform(dataSet);

        return dataSet.getFeatures(); // 返回标准化后的特征
    }
}

这段代码的功能是处理图像,将图像转换为适合深度学习模型输入的格式,并对图像进行标准化处理。下面我们将详细解释每一部分的代码和背后的原理。

1. 创建空的INDArray

int width = 28;
int height = 28;
INDArray array = Nd4j.zeros(1, width * height);

这段代码的目的是为图像数据创建一个INDArray,也就是一个二维数组。在深度学习中,数据通常以数组的形式输入模型。

  • widthheight:这里设置了图像的宽度和高度为28x28像素。28x28是MNIST数据集中的图像尺寸(手写数字的大小),并且深度学习模型通常要求输入的数据大小一致。
  • INDArray:是由ND4J(一个用于处理多维数组的库)创建的数组,类似于numpy.ndarray。这里我们创建一个形状为1x784的INDArray,它代表了图像数据的平铺形式(28 * 28 = 784)。将图像的每个像素值存储在这个数组里。

2. 将图像转换为灰度图像

for (int i = 0; i < width; i++) {
    for (int j = 0; j < height; j++) {
        int rgb = image.getRGB(i, j);
        int r = (rgb >> 16) & 0xFF;
        int g = (rgb >> 8) & 0xFF;
        int b = rgb & 0xFF;
        // 灰度转换公式
        double gray = (0.299 * r + 0.587 * g + 0.114 * b) / 255.0;
        array.putScalar(new int[]{0, j * width + i}, gray);
    }
}

这部分代码的目的是将图像的每个像素值转换为灰度值,因为在处理手写数字识别任务时,颜色信息(如红、绿、蓝)对识别数字的帮助有限,使用灰度图像就足够了。

  • image.getRGB(i, j):获取图像上第(i, j)位置的RGB颜色值。
  • rgb >> 16 & 0xFFrgb >> 8 & 0xFFrgb & 0xFF:分别提取RGB颜色的红、绿、蓝分量。每个分量都由8位二进制组成,0xFF用于提取每个分量的低8位。
  • 灰度转换公式:(0.299 * r + 0.587 * g + 0.114 * b)是将RGB值转换为灰度值的标准公式。该公式根据人眼对不同颜色的敏感度对红、绿、蓝三个通道的权重进行了加权。
  • 例如,绿色分量在视觉上比红色和蓝色更重要,因此它的权重为0.587。
  • array.putScalar(new int[]{0, j * width + i}, gray):将计算出的灰度值存储到INDArray数组中。j * width + i计算出当前像素的在一维数组中的位置,0表示这是第一张图像(批量处理时可能有多张图像)。

3. 计算标准化参数(拟合标准化器)

INDArray sampleData = Nd4j.rand(new int[]{100, width * height});
DataSet sampleDataSet = new DataSet(sampleData, null);
NormalizerStandardize normalizer = new NormalizerStandardize();
normalizer.fit(sampleDataSet); // 计算标准化参数

这一部分的目的是通过一组随机数据来计算标准化参数。标准化是深度学习中常用的一种预处理方法,目的是让数据的分布更符合模型的要求,通常是将数据的均值调整为0,标准差调整为1。

  • Nd4j.rand(new int[]{100, width * height}):生成一个大小为100x784的随机数组。这些数据并不是真实的图像数据,只是用来模拟真实数据的统计特性。这里的随机数据相当于假设我们在训练时会遇到的输入数据。
  • new DataSet(sampleData, null):将随机生成的数据包装成一个DataSet对象。DataSet是一个包含特征和标签的容器。这里没有标签,只有特征数据。
  • NormalizerStandardize normalizer = new NormalizerStandardize():创建一个标准化器对象。NormalizerStandardize是ND4J提供的一个标准化工具,它会自动计算数据的均值和标准差,并应用标准化操作。
  • normalizer.fit(sampleDataSet):使用模拟数据(sampleDataSet)来计算标准化的参数(均值和标准差)。这个步骤会“拟合”标准化器,确保我们接下来的图像数据能够进行正确的标准化处理。

4. 归一化图像数据

DataSet dataSet = new DataSet(array, null);
normalizer.transform(dataSet);

现在我们已经有了一个训练好的标准化器,可以用它来对图像进行标准化处理。

  • new DataSet(array, null):将预处理后的图像数据(已经转换为灰度值的28x28像素数据)包装成一个DataSet对象,注意这里没有标签(null)。
  • normalizer.transform(dataSet):使用前面计算出的标准化参数,处理我们当前的图像数据。transform方法会根据均值和标准差,将图像数据进行标准化操作。

5. 返回处理后的数据

return dataSet.getFeatures(); // 返回标准化后的特征

这行代码返回处理后的图像数据,dataSet.getFeatures()获取了经过标准化处理的特征数据。这个特征就是接下来输入到深度学习模型中的数据。

6. 总结图像处理的整体过程

  1. 图像转灰度:从RGB图像中提取出每个像素的灰度值,标准化图像信息。
  2. 创建一个空的**INDArray**:存储图像的像素值,并为接下来的计算做准备。
  3. 标准化处理:为了确保数据更符合深度学习模型的要求,使用NormalizerStandardize来标准化图像数据。
  4. 返回标准化数据:将标准化后的图像数据返回,准备将其输入到训练好的深度学习模型进行预测。

图像处理的核心目的是将图像数据转换为适合深度学习模型处理的格式,同时通过标准化减少数据的偏差,确保模型能够更快收敛,并提高预测的准确性。

Spring Boot 控制器

接下来,我们需要一个控制器来处理用户上传的图片,并调用模型进行预测。

package com.neo.controller;

import com.neo.service.ImageProcessingService;
import com.neo.service.OCRPredictionService;
import org.nd4j.linalg.api.ndarray.INDArray;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

@Controller
public class OCRController {

    @Autowired
    private ImageProcessingService imageProcessingService;

    @Autowired
    private OCRPredictionService ocrPredictionService;

    // 文件保存路径,您可以根据实际需求修改此路径
    private static final String UPLOAD_DIR = "src/main/resources/static/uploads/";

    // 显示上传页面
    @RequestMapping("/ocr")
    public String showUploadPage() {
        return "upload";  // 返回上传页面的视图
    }

    // 处理上传的图片并进行 OCR 预测
    @PostMapping("/ocr/predict")
    public String predict(@RequestParam("file") MultipartFile file, Model model) {
        try {

            String fileName = file.getOriginalFilename();

            // 创建保存文件的路径
            Path uploadPath = Paths.get(UPLOAD_DIR);
            if (!Files.exists(uploadPath)) {
                Files.createDirectories(uploadPath);  // 创建目录
            }
            // 保存文件到本地
            Path filePath = uploadPath.resolve(fileName);
            file.transferTo(filePath);

            // 读取保存的文件
            // 读取图片
            BufferedImage image = ImageIO.read(filePath.toFile());
            // 处理图片并预测
            INDArray processedImage = imageProcessingService.preprocessImage(image);
            // 使用模型进行预测
            int predictedDigit = ocrPredictionService.predict(processedImage);

            System.out.println("识别的数字是: " + predictedDigit);
            // 将图片和预测结果传递给前端
            model.addAttribute("imagePath", fileName);  // 只传递相对路径
            model.addAttribute("prediction", predictedDigit);

            return "upload";  // 返回上传页面并显示预测结果
        } catch (IOException e) {
            e.printStackTrace();
            model.addAttribute("error", "图片处理失败,请重新上传");
            return "upload";  // 如果出现错误,返回上传页面
        }
    }
}

构建前端界面

为了让用户能够方便地上传图片,我们需要一个友好的用户界面。使用ThymeleafBootstrap,我们可以快速构建一个简洁的上传页面。

HTML模板代码

通过这个简单的表单,用户可以上传图片,然后在页面上查看预测结果。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>OCR 图像上传和预测</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
    <h2 class="text-center">手写数字识别 OCR</h2>

    <!-- 上传图片表单 -->
    <form action="/ocr/predict" method="post" enctype="multipart/form-data" class="mt-4">
        <div class="mb-3">
            <label for="file" class="form-label">选择图片文件</label>
            <input type="file" class="form-control" id="file" name="file" required>
        </div>
        <button type="submit" class="btn btn-primary">上传并预测</button>
    </form>

    <!-- 错误信息 -->
    <div th:if="${error}" class="alert alert-danger mt-3" role="alert">
        <p th:text="${error}"></p>
    </div>

    <!-- 显示上传的图片 -->
    <div th:if="${imagePath}">
        <h3 class="mt-4">上传的图片:</h3>
        <img th:src="@{/uploads/{image}(image=${imagePath})}" alt="Uploaded Image" class="img-fluid">
    </div>

    <div th:if="${prediction != null}">
        <h3 class="mt-4">预测结果:</h3>
        <div class="alert alert-success" role="alert">
            识别结果:<strong th:text="${prediction}"></strong>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

效果演示

在这里插入图片描述

总结

我们完成了一个从模型训练到Web应用集成的完整流程,展示了如何使用Spring Boot和DeepLearning4J构建一个手写数字识别系统。这个项目不仅展示了深度学习在实际应用中的潜力,也展示了Java开发环境中集成深度学习技术的可能性。

通过这个示例,相信你对深度学习有了更加直观的理解,也希望你能够在此基础上,进一步探索深度学习和Java的结合应用。无论是扩展这个项目的功能,还是尝试不同的数据集和模型结构,都是很好的学习和实践方式。

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

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

相关文章

天机学堂5-XxlJobRedis

文章目录 梳理前面的实现&#xff1a;Feign点赞改进 day07-积分系统bitmap相关命令签到增加签到记录计算本月已连续签到的天数查询签到记录 积分表设计签到-->发送RabbitMQ消息&#xff0c;保存积分对应的消费者&#xff1a;**消费消息 用于保存积分**增加积分查询个人今日积…

2024 年度学习总结

目录 1. 前言 2. csdn 对于我的意义 3. 写博客的初衷 3.1 现在的想法 4. 写博客的意义 5. 关于生活和博客创作 5.1 写博客较于纸质笔记的优势 6. 致 2025 1. 前言 不知不觉, 来到 csdn 已经快一年了, 在这一年中, 我通过 csdn 学习到了很多知识, 结识了很多的良师益友…

使用Chrome和Selenium实现对Superset等私域网站的截图

最近遇到了一个问题&#xff0c;因为一些原因&#xff0c;我搭建的一个 Superset 的 Report 功能由于节假日期间不好控制邮件的发送&#xff0c;所以急需一个方案来替换掉 Superset 的 Report 功能 首先我们需要 Chrome 浏览器和 Chrome Driver&#xff0c;这是执行数据抓取的…

[操作系统] 进程的调度

进程切换概念 时间⽚&#xff1a;当代计算机都是分时操作系统&#xff0c;没有进程都有它合适的时间⽚(其实就是⼀个计数 器)。时间⽚到达&#xff0c;进程就被操作系统从CPU中剥离下来。 死循环是如何运行&#xff1f; 当一个进程代码为死循环&#xff0c;它并不会一直占据C…

Biotin sulfo-N-hydroxysuccinimide ester ;生物素磺基-N-羟基琥珀酰亚胺酯;生物素衍生物;190598-55-1

一、生物素及其衍生物的概述 生物素衍生物是指在生物素&#xff08;Vitamin H或B7&#xff09;分子基础上进行化学修饰得到的衍生化合物。这些衍生化合物在生物医学研究、临床诊断和药物开发等领域有着广泛的应用。 生物素&#xff08;Biotin&#xff09;是一种水溶性维生素&a…

Jenkins-Pipeline简述

一. 什么是Jenkins pipeline&#xff1a; pipeline在jenkins中是一套插件&#xff0c;主要功能在于&#xff0c;将原本独立运行于单个或者多个节点的任务连接起来&#xff0c;实现单个任务难以完成的复杂发布流程。Pipeline的实现方式是一套Groovy DSL&#xff0c;任何发布流程…

Linux系统下安装配置Nginx(保姆级教程)

目录 前言 安装配置Nginx 一.下载依赖 二.下载Nginx 1. 访问官网?&#xff0c;获取需要的Nginx版本 2. 将文件下载到Linux系统 3. 解压文件 4. 解压成功后&#xff0c;当前文件夹会出现一个nginx-1.26.1文件夹&#xff0c;进入到文件夹内 5. 配置nginx 6.?编译并安…

《Linux服务与安全管理》| 邮件服务器安装和配置

《Linux服务与安全管理》| 邮件服务器安装和配置 目录 《Linux服务与安全管理》| 邮件服务器安装和配置 1.在Server01上安装dns、postfix、dovecot和telnet&#xff0c;并启动 2&#xff0e;在Server01上配置DNS服务器&#xff0c;设置MX资源记录 3&#xff0e;在server1上…

WPS数据分析000001

目录 一、表格的新建、保存、协作和分享 新建 保存 协作 二、认识WPS表格界面 三、认识WPS表格选项卡 开始选项卡 插入选项卡 页面布局选项卡 公式选项卡 数据选项卡 审阅选项卡 视图选项卡 会员专享选项卡 一、表格的新建、保存、协作和分享 新建 ctrlN------…

2025年免费量化交易软件——PTrade(含开通攻略)

量化交易软件&#xff0c;为广大投资者提供了一个便捷、高效的投资工具。 本文重点为大家介绍一款2025年好用的免费量化交易软件&#xff1a;PTrade量化&#xff0c;并详解其功能、特点、开通方法等。 一、PTrade的概念 PTrade是恒生电子开发的一款交易终端软件&#xff0c;旨…

【数据结构篇】顺序表 超详细

目录 一.顺序表的定义 1.顺序表的概念及结构 1.1线性表 2.顺序表的分类 2.1静态顺序表 2.2动态顺序表 二.动态顺序表的实现 1.准备工作和注意事项 2.顺序表的基本接口&#xff1a; 2.0 创建一个顺序表 2.1 顺序表的初始化 2.2 顺序表的销毁 2.3 顺序表的打印 3.顺序…

mysql查缺补漏

auto increment&#xff1a;自增序列&#xff0c;在字段后作为约束使用 comment&#xff1a;备注信息&#xff0c;用于在创建字段后或创建表的语句最后. 数值类型&#xff1a; 字符串类型&#xff1a; 日期类型&#xff1a; desc table_name&#xff1a;查询表结构 sho…

C++ 面向对象(继承)

三、继承 3.1 继承的概念 基于一个已有的类 去重新定义一个新的类&#xff0c;这种方式我们叫做继承 关于继承的称呼 一个类B 继承来自 类 A 我们一般称呼 A类&#xff1a;父类 基类 B类: 子类 派生类 B继承自A A 派生了B 示例图的语法 class vehicle // 车类 {}class …

JAVA-IO模型的理解(BIO、NIO)

前言 &#xff08;本文是作者学习制作rpc框架时&#xff0c;一些自用的笔记&#xff0c;并不会完整详细的介绍某个模块&#xff0c;会写大概的流程及一些相关概念&#xff0c;供日后复习使用~&#xff09; IO模型 先理解基本的IO流程&#xff1a; 应用A把消息发送到 TCP发送缓…

【Spring】原型 Bean 被固定

问题描述 在定义 Bean 时&#xff0c;有时候我们会使用原型 Bean&#xff0c;例如定义如下&#xff1a; Service Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class ServiceImpl { }然后我们按照下面的方式去使用它&#xff1a; RestController public class Hello…

奉加微PHY6230兼容性:部分手机不兼容

从事嵌入式单片机的工作算是符合我个人兴趣爱好的,当面对一个新的芯片我即想把芯片尽快搞懂完成项目赚钱,也想着能够把自己遇到的坑和注意事项记录下来,即方便自己后面查阅也可以分享给大家,这是一种冲动,但是这个或许并不是原厂希望的,尽管这样有可能会牺牲一些时间也有哪天原…

Python脚本实现通过JLink烧录Hex文件

1 安装JLink驱动程序 驱动安装包下载路径&#xff1a;https://www.segger.com/downloads/jlink/ 选择对应的版本下载&#xff1a; 将下载的安装文件双击进行安装。 2 安装 pylink 包 pip install pylink3 查询 JLink 设备的 serial number 将JLink通过USB线插入电脑。 w…

【Qt】04-Lambda表达式

前言一、概念引入二、使用方法2.1 基本用法代码示例2.2 捕获外部变量2.3 参数列表 三、完整代码mywidget.cppsecondwidget.cppmywidget.hsecondwidget.h 总结 前言 一、概念引入 Lambda表达式&#xff08;Lambda Expressions&#xff09;是C11标准引入的一种匿名函数对象&…

[STM32 HAL库]串口中断编程思路

一、前言 最近在准备蓝桥杯比赛&#xff08;嵌入式赛道&#xff09;&#xff0c;研究了以下串口空闲中断DMA接收不定长的数据&#xff0c;感觉这个方法的接收效率很高&#xff0c;十分好用。方法配置都成功了&#xff0c;但是有一个点需要进行考虑&#xff0c;就是一般我们需要…

汇编与逆向(一)-汇编工具简介

RadASM是一款著名的WIN32汇编编辑器&#xff0c;支持MASM、TASM等多种汇编编译器&#xff0c;Windows界面&#xff0c;支持语法高亮&#xff0c;自带一个资源编辑器和一个调试器。 一、汇编IDE工具&#xff1a;RadASM RadASM有内置的语言包 下载地址&#xff1a;RadASM asse…