Minio 是一个基于Apache License v2.0开源协议的对象存储服务。它可以运行在多种操作系统上,包括 Linux 和 Windows 等。
它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5TB不等。

Minio官网:
https://min.io/
中文地址:
https://www.minio.org.cn
中文文档:
https://www.minio.org.cn/docs/minio/linux/index.html
演示:
https://play.minio.org.cn
用户名:minioadmin、密码minioadmin

MinIO Linux 安装

单节点部署

Download:
https://www.minio.org.cn/download.shtml#/linux
使用以下命令下载安装最新版本的稳定 MinIO二进制包, 并设置 $PATH :

[root@localhost ~]# wget https://dl.minio.org.cn/server/minio/release/linux-amd64/minio
[root@localhost ~]# chmod +x minio
[root@localhost ~]# sudo mv minio /usr/local/bin/

image

创建 systemd 系统启动服务文件

创建 minio.service 启动文件,确保文件在
/usr/lib/systemd/system/minio.service

[root@localhost ~]# vi /usr/lib/systemd/system/minio.service

minio.service

[Unit]
Description=MinIO
Documentation=https://min.io/docs/minio/linux/index.html
Wants=network-online.target
After=network-online.target
AssertFileIsExecutable=/usr/local/bin/minio

[Service]
WorkingDirectory=/usr/local

User=minio-user
Group=minio-user
ProtectProc=invisible

EnvironmentFile=-/etc/default/minio
ExecStartPre=/bin/bash -c "if [ -z \"${MINIO_VOLUMES}\" ]; then echo \"Variable MINIO_VOLUMES not set in /etc/default/minio\"; exit 1; fi"
ExecStart=/usr/local/bin/minio server $MINIO_OPTS $MINIO_VOLUMES

# MinIO RELEASE.2023-05-04T21-44-30Z adds support for Type=notify (https://www.freedesktop.org/software/systemd/man/systemd.service.html#Type=)
# This may improve systemctl setups where other services use `After=minio.server`
# Uncomment the line to enable the functionality
# Type=notify

# Let systemd restart this service always
Restart=always

# Specifies the maximum file descriptor number that can be opened by this process
LimitNOFILE=65536

# Specifies the maximum number of threads this process can create
TasksMax=infinity

# Disable timeout logic and wait until process is stopped
TimeoutStopSec=infinity
SendSIGKILL=no

[Install]
WantedBy=multi-user.target

# Built for ${project.name}-${project.version} (${project.name})

默认情况下,
minio.service
文件以
minio-user
用户和组的身份运行。 您可以使用
groupadd

useradd
创建用户和组。 命令。下面的示例创建了用户和组,并设置了权限 来访问供 MinIO 使用的文件夹路径。这些命令通常 需要 root ( sudo ) 权限。

[root@localhost ~]# groupadd -r minio-user
[root@localhost ~]# useradd -M -r -g minio-user minio-user
# 存储路径
[root@localhost ~]# chown minio-user:minio-user /mnt/data
chown: 无法访问"/mnt/data": 没有那个文件或目录
[root@localhost ~]# mkdir /mnt/data
[root@localhost ~]# chown minio-user:minio-user /mnt/data

本例中的驱动器路径由 MINIO_VOLUMES 环境变量指定。更改此处和环境变量文件中的值,使其与 MinIO 打算使用的驱动器路径相匹配。

创建环境变量文件


/etc/default/minio
创建环境变量文件。 MinIO 服务器容器可以将此文件作为所有
environment variables
下面的示例提供了一个起始环境文件:
密码不能小于8个字符, 否则无法启动

[root@localhost ~]# vi /etc/default/minio
# MINIO_ROOT_USER and MINIO_ROOT_PASSWORD sets the root account for the MinIO server.
# This user has unrestricted permissions to perform S3 and administrative API operations on any resource in the deployment.
# Omit to use the default values 'minioadmin:minioadmin'.
# MinIO recommends setting non-default values as a best practice, regardless of environment
# 用户名长度不能小于3个字符
MINIO_ROOT_USER=admin
# 密码不能小于8个字符, 否则无法启动
MINIO_ROOT_PASSWORD=minioadmin

# MINIO_VOLUMES sets the storage volume or path to use for the MinIO server.

MINIO_VOLUMES="/mnt/data"

# MINIO_OPTS sets any additional commandline options to pass to the MinIO server.
# For example, `--console-address :9001` sets the MinIO Console listen port
MINIO_OPTS="--console-address :9001"

启动MinIO服务

启动 MinIO SNSD 部署即服务:

[root@localhost ~]# sudo systemctl start minio.service

使用以下命令确认服务是否在线和功能正常:

[root@localhost ~]# sudo systemctl status minio.service
[root@localhost ~]# journalctl -f -u minio.service

自启动,将进程作为主机引导的一部分,在服务器重启的过程中该进程会自动重启,而不用再进行手动管理。

[root@localhost ~]# sudo systemctl enable minio.service

image

连接到MinIO服务

浏览器中输入:
http://localhost:9001
登录MinIO的用户名和密码配置参数为
MINIO_ROOT_USER

MINIO_ROOT_PASSWORD
这些配置可以在在容器指定的环境文件中进行修改。
image
image
您可以使用MinIO控制台进行一般管理任务,如身份和访问管理、指标和日志监控或服务器配置。每个MinIO服务器都包含其自己的嵌入式MinIO控制台。 如果您的本地主机防火墙允许外部访问控制台端口,则同一网络上的其他主机可以使用您的本地主机的IP地址或主机名访问控制台。

SpringBoot项目整合MinIO

https://mvnrepository.com/artifact/io.minio/minio

配置项

添加依赖

<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.5.10</version>
</dependency>

application.yml 配置 minio


spring:
     mvc:
      hiddenmethod:
        filter:
          enabled: true
  #设置文件上传大小限制
  servlet:
    multipart:
      max-file-size: -1    #设置单个文件的大小 -1表示不限制
      max-request-size: -1    #单次请求的文件的总大小 -1表示不限制

minio:
  endpoint: http://172.16.3.195:9000
  accessKey: admin
  secretKey: minioadmin
  bucketName: vipsoft-devminioadmin

MinioConfig.java

package com.vipsoft.oss.config;


import io.minio.MinioClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioConfig {
    /**
     * 服务地址:http://172.16.3.195:9000
     */
    private String endpoint;

    /**
     * 用户名
     */
    private String accessKey;

    /**
     * 密码
     */
    private String secretKey;

    /**
     * 存储桶名称
     */
    private String bucketName;


    @Bean
    public MinioClient getMinioClient() {
        MinioClient minioClient = MinioClient.builder().endpoint(endpoint)
                .credentials(accessKey, secretKey)
                .build();
        return minioClient;
    }

    //todo 省去 getter & setter
}

工具类

MinioUtil.java

package com.vipsoft.oss.util;

import cn.hutool.core.io.IoUtil;
import com.cuwor.oss.config.MinioConfig;
import io.minio.*;
import io.minio.MinioClient;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Component
public class MinioUtil {

    @Autowired
    private MinioClient minioClient;

    @Autowired
    private MinioConfig minioConfig;

    private static final int DEFAULT_EXPIRY_TIME = 7 * 24 * 3600;

    /**
     * 判断bucket是否存在
     */
    public boolean bucketExists(String bucketName) {
        boolean exists;
        try {
            exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
        } catch (Exception e) {
            e.printStackTrace();
            exists = false;
        }
        return exists;
    }


    /**
     * 创建存储桶,存在则不创建
     */
    public boolean makeBucket(String bucketName) {
        boolean exists;
        try {
            exists = bucketExists(bucketName);
            if (!exists) {
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
                exists = true;
            }
        } catch (Exception e) {
            e.printStackTrace();
            exists = false;
        }
        return exists;
    }

    /**
     * 删除存储桶 -- 有文件,不让删除
     *
     * @param bucketName 存储桶名称
     * @return boolean
     */
    public boolean removeBucket(String bucketName) {
        try {
            boolean flag = bucketExists(bucketName);
            if (flag) {
                Iterable<Result<Item>> myObjects = listObjects(bucketName);
                for (Result<Item> result : myObjects) {
                    Item item = result.get();
                    // 有对象文件,则删除失败
                    if (item.size() > 0) {
                        return false;
                    }
                }
                // 删除存储桶,注意,只有存储桶为空时才能删除成功。
                minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
                flag = bucketExists(bucketName);
                if (!flag) {
                    return true;
                }
            }
            return false;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 列出存储桶中的所有对象
     *
     * @param bucketName 存储桶名称
     * @return Iterable<Result < Item>>
     */
    public Iterable<Result<Item>> listObjects(String bucketName) {
        boolean flag = bucketExists(bucketName);
        if (flag) {
            return minioClient.listObjects(ListObjectsArgs.builder().bucket(bucketName).build());
        }
        return null;
    }


    /**
     * 列出所有存储桶名称
     *
     * @return List<String>
     */
    public List<String> listBucketNames() {
        List<String> bucketListName = new ArrayList<>();
        try {
            List<Bucket> bucketList = minioClient.listBuckets();
            for (Bucket bucket : bucketList) {
                bucketListName.add(bucket.name());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return bucketListName;
    }

    /**
     * 列出存储桶中的所有对象名称
     *
     * @param bucketName 存储桶名称
     * @return List<String>
     */
    public List<String> listObjectNames(String bucketName) {
        List<String> listObjectNames = new ArrayList<>();
        try {
            boolean flag = bucketExists(bucketName);
            if (flag) {
                Iterable<Result<Item>> myObjects = listObjects(bucketName);
                for (Result<Item> result : myObjects) {
                    Item item = result.get();
                    listObjectNames.add(item.objectName());
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return listObjectNames;
    }


    /**
     * 获取对象的元数据
     *
     * @param objectName 存储桶里的对象名称
     * @return
     */
    public StatObjectResponse statObject(String objectName) {
        StatObjectResponse statObject = null;
        try {
            statObject = minioClient.statObject(StatObjectArgs.builder().bucket(minioConfig.getBucketName()).object(objectName).build());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return statObject;
    }

    //region 上传文件

    /**
     * 上传文件
     *
     * @param file       文件
     * @param objectName 文件名称
     */
    public boolean uploadObject(MultipartFile file, String objectName) {
        // 使用putObject上传一个文件到存储桶中。
        try {
            InputStream inputStream = file.getInputStream();
            minioClient.putObject(PutObjectArgs.builder()
                    .bucket(minioConfig.getBucketName())
                    .object(objectName)
                    .stream(inputStream, file.getSize(), -1)
                    .contentType(file.getContentType())
                    .build());
            IoUtil.close(inputStream);
//            return StrUtil.format("{}/{}/{}", minioConfig.getEndpoint(), minioConfig.getBucketName(), objectName);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 通过InputStream上传对象
     *
     * @param objectName  存储桶里的对象名称
     * @param inputStream 要上传的流
     * @param contentType 上传的文件类型 例如 video/mp4  image/jpg
     * @return boolean
     */
    public boolean uploadObject(InputStream inputStream, String objectName, String contentType) {
        try {
            minioClient.putObject(PutObjectArgs.builder().bucket(minioConfig.getBucketName()).object(objectName).stream(
                            //不清楚文件的大小时,可以传-1,10485760。如果知道大小也可以传入size,partsize。
                            inputStream, -1, 10485760)
                    .contentType(contentType)
                    .build());
            IoUtil.close(inputStream);
            StatObjectResponse statObject = statObject(objectName);
            if (statObject != null && statObject.size() > 0) {
                return true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 通过文件上传到对象
     *
     * @param objectName 存储桶里的对象名称
     * @param fileName   File name
     * @return boolean
     */
    public boolean uploadObject(String objectName, String fileName) {
        try {
            minioClient.uploadObject(UploadObjectArgs.builder()
                    .bucket(minioConfig.getBucketName())
                    .object(objectName)
                    .filename(fileName)
                    .build());
            StatObjectResponse statObject = statObject(objectName);
            if (statObject != null && statObject.size() > 0) {
                return true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return false;
    }

    //endregion


    /**
     * 获取文件访问地址
     *
     * @param fileName 文件名称
     */
    public String getPresignedObjectUrl(String fileName) {
        try {
            return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                    .method(Method.GET)
                    .bucket(minioConfig.getBucketName())
                    .object(fileName)
                    .build()
            );
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public String getObjectUrl(String objectName) {
        String url = "";
        try {
            url = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                    .method(Method.GET)
                    .bucket(minioConfig.getBucketName())
                    .object(objectName)
                    .build());

        } catch (Exception e) {
            e.printStackTrace();
        }
        return url;
    }

    /**
     * 生成一个给HTTP GET请求用的presigned URL。
     * 浏览器/移动端的客户端可以用这个URL进行下载,即使其所在的存储桶是私有的。这个presigned URL可以设置一个失效时间,默认值是7天。
     *
     * @param objectName 存储桶里的对象名称
     * @param expires    失效时间(以小时单位),默认是7天,不得大于七天
     * @return
     */
    public String getObjectUrl(String objectName, Integer expires) {
        String url = "";
        try {
            if (expires < 1 || expires > DEFAULT_EXPIRY_TIME) {
                throw new Exception("Expires must be in range of 1 to " + DEFAULT_EXPIRY_TIME);
            }
            url = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                    .method(Method.GET)
                    .bucket(minioConfig.getBucketName())
                    .object(objectName)
                    .expiry(expires, TimeUnit.HOURS)//动态参数
                    // .expiry(24 * 60 * 60)//用秒来计算一天时间有效期
                    // .expiry(1, TimeUnit.DAYS)//按天传参
                    // .expiry(1, TimeUnit.HOURS)//按小时传参数
                    .build());

        } catch (Exception e) {
            e.printStackTrace();
        }
        return url;
    }

    /**
     * 下载文件,通过 HttpServletResponse 返回
     *
     * @param objectName 存储桶里的对象名称
     */
    public void downloadObject(HttpServletResponse response, String objectName) {
        try {
            InputStream file = minioClient.getObject(GetObjectArgs.builder()
                    .bucket(minioConfig.getBucketName())
                    .object(objectName)
                    .build());
            response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(objectName, "UTF-8"));
            ServletOutputStream servletOutputStream = response.getOutputStream();
            int len;
            byte[] buffer = new byte[1024];
            while ((len = file.read(buffer)) > 0) {
                servletOutputStream.write(buffer, 0, len);
            }
            servletOutputStream.flush();
            file.close();
            servletOutputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * 下载并将文件保存到本地
     *
     * @param objectName 存储桶里的对象名称
     * @param fileName   下载保存的文件名
     * @return boolean
     */
    public boolean downloadObject(String objectName, String fileName) {
        try {
            StatObjectResponse statObject = statObject(objectName);
            if (statObject != null && statObject.size() > 0) {
                minioClient.downloadObject(DownloadObjectArgs.builder()
                        .bucket(minioConfig.getBucketName())
                        .object(objectName)
                        .filename(fileName)
                        .build());
                return true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }


    /**
     * 以流的形式获取一个文件对象
     *
     * @param bucketName 存储桶名称
     * @param objectName 存储桶里的对象名称
     * @return InputStream
     */
    public InputStream getObject(String bucketName, String objectName) {
        try {
            StatObjectResponse statObject = statObject(objectName);
            if (statObject != null && statObject.size() > 0) {
                InputStream stream = minioClient.getObject(GetObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .build());
                return stream;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 以流的形式获取一个文件对象(断点下载)
     *
     * @param bucketName 存储桶名称
     * @param objectName 存储桶里的对象名称
     * @param offset     起始字节的位置
     * @param length     要读取的长度 (可选,如果无值则代表读到文件结尾)
     * @return InputStream
     */
    public InputStream getObject(String bucketName, String objectName, long offset, Long length) {
        try {
            StatObjectResponse statObject = statObject(objectName);
            if (statObject != null && statObject.size() > 0) {
                InputStream stream = minioClient.getObject(GetObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .offset(1024L)
                        .length(4096L)
                        .build());
                return stream;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }


    /**
     * 删除一个对象
     *
     * @param objectName 存储桶里的对象名称
     */
    public boolean removeObject(String objectName) {
        try {
            minioClient.removeObject(RemoveObjectArgs.builder().bucket(minioConfig.getBucketName()).object(objectName).build());
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 删除指定桶的多个文件对象,返回删除错误的对象列表,全部删除成功,返回空列表
     *
     * @param bucketName  存储桶名称
     * @param objectNames 含有要删除的多个object名称的迭代器对象 eg:
     *                    List<DeleteObject> objects = new LinkedList<>();
     *                    objects.add(new DeleteObject("my-objectname1"));
     *                    objects.add(new DeleteObject("my-objectname2"));
     *                    objects.add(new DeleteObject("my-objectname3"));
     * @return 如果有值,说明当前文件删除失败
     */
    public List<String> removeObjects(String bucketName, List<DeleteObject> objectNames) {
        List<String> deleteErrorNames = new ArrayList<>();
        try {
            Iterable<Result<DeleteError>> results = minioClient.removeObjects(RemoveObjectsArgs.builder().bucket(bucketName).objects(objectNames).build());
            for (Result<DeleteError> result : results) {
                DeleteError error = result.get();
                deleteErrorNames.add(error.objectName());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return deleteErrorNames;
    }
}


测试

package com.vipsoft.admin;

import cn.hutool.core.util.StrUtil;
import com.cuwor.oss.util.MinioUtil;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.Assert;

@SpringBootTest
public class MinioTest {

    Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private MinioUtil minioUtil;


    @Test
    void makeBucketTest() {
        boolean flag = minioUtil.makeBucket("public");
        Assert.isTrue(flag, "查询异常");
    }


    @Test
    void uploadObjectTest() {
        boolean flag = minioUtil.uploadObject("/avatar/123.png", "D:\\Users\\Pictures\\R-C.png");
        Assert.isTrue(flag, "上传异常");
    }


    @Test
    void getObjectUrlTest() {
        String objectName="/avatar/123.png";
        String url = minioUtil.getObjectUrl(objectName);
        logger.info("url:{}", url);
        url = minioUtil.getObjectUrl(objectName, 3);
        logger.info("expires url:{}", url);
        Assert.isTrue(StrUtil.isNotEmpty(url), "上传异常");
    }
}

image

Taro 4.0 已经推出一段时间了,4.0 版本主要是支持了鸿蒙端的开发以及 Vite 编译工具的支持。duxapp 在这段时间也跟随 Taro 的脚步,实现的对鸿蒙端的支持,并且也将之前的 duxui 这个多端的 UI 库,对鸿蒙端实现了兼容。

duxui 组件库提供了 60+ 的组件支持,能快速帮助你完成业务。

现在使用这个 UI 库,不仅能开发鸿蒙,还能实现同时开发 React Native、小程序和 H5,也是目前唯一一个能兼容这么多端的 UI 库。

组件展示

效果图

如何使用

使用下面的命令,可以快速初始化一个 UI 库的示例项目,你可以通过这个示例项目快速的查看到 UI 库在鸿蒙端以及其他端的效果

npx duxapp-cli create projectExample duxuiExample
  • 提示:在这之前需要安装好基本的环境
    nodejs 20+
    yarn
    git

创建项目后,进入项目目录
projectExample
运行以下命令

yarn dev:harmony --app=duxuiExample

编译完成后,使用 DevEco Studio 打开
dist/harmony
目录,这个目录就是一个原生鸿蒙项目

  • 如果你还不了解鸿蒙开发基础知识,或者想继续深入了解如何开发,可以查看这篇
    入门教程

duxapp 做了些什么

自动化

在 duxapp 中,我们把鸿蒙工程模板做了封装,就像
React Native
端那样,你不需要自行创建项目工程文件,在你运行鸿蒙相关的命令的时候,会自动创建鸿蒙工程项目,你需要做的就是使用 DevEco Studio 来继续编译 duxapp 生成的鸿蒙工程文件

配置化

包名、版本等信息就和
React Native
端那样,通过配置文件来实现

模块化

我们将鸿蒙端的支持封装到了
duxapp
的一个模块
duxappHarmony
中,只要你依赖了这个模块,就能实现对鸿蒙端的兼容

如果你还不是很了解 duxapp 的模块化,可以查看
模块化介绍

UI组件库

在过去这一段时间的兼容过程中,主要做的工作就是对 UI 库的兼容,因为鸿蒙的
arkui
和 Web 标准对比,存在不小的差异,每个组件都需要进行适配,并且还需要对一些底层代码进行修改。总得来说,现阶段虽然还存在一些小问题,但是95%的功能已经实现兼容

继续

如果你对这个项目有兴趣,可以查看文档,继续了解详情

开发文档:
http://duxapp.cn

GitHub:
https://github.com/duxapp

来源:晓飞的算法工程笔记 公众号,转载请注明出处

论文: VL4AD: Vision-Language Models Improve Pixel-wise Anomaly Detection

创新性


  1. 提出
    VL4AD
    模型用于解决语义分割网络难以检测来自未知语义类别的异常的问题,避免额外的数据收集和模型训练。
  2. VL4AD
    将视觉-语言(
    VL
    )编码器纳入现有的异常检测器,利用语义广泛的
    VL
    预训练来增强对离群样本的感知,还加入
    max-logit
    提示集成和类别合并策略用于丰富类别描述。
  3. 提出了一种新的评分函数,可通过文本提示实现无数据和无训练的离群样本监督。

VL4AD


视觉文本编码器

视觉编码器
\(\mathcal{E}_\text{vision, vis-lang}\)
是与文本编码器
\(\mathcal{E}_\text{text}\)
共同预训练,解码器
\(\mathcal{D}_\text{vis-lang}\)
处理多尺度的视觉和文本嵌入,生成两种类型的输出:掩码预测分数
\(\mathbf{s} \in [0, 1]^{N\times H\times W}\)
和掩码分类分数
\(\mathbf{c} \in [0, 1]^{N\times K}\)
,其中
\(N\)
表示对象查询的数量。

对象查询是可学习的嵌入,类似于目标检测网络中的先验框。掩码预测分数以类别无关的方式识别物体,而掩码分类分数计算掩码属于特定语义类别的概率。

基于编码后的视觉嵌入
\(\mathbf{v}_i\)

\(i=1, \dots, N\)
)和
ID
类别文本嵌入
\(\mathbf{t}_j\)

\(j=1, \dots, K\)
)之间的余弦相似性计算掩码分类分数:

\[\begin{equation}
\mathbf{c}_{i} = \text{softmax}\Big(1/T
\begin{bmatrix}
\text{cos}(\mathbf{v}_i, \mathbf{t}_1), &
\text{cos}(\mathbf{v}_i, \mathbf{t}_2), &
\ldots, &
\text{cos}(\mathbf{v}_i, \mathbf{t}_{K})
\end{bmatrix}
\Big)
\end{equation}
\]

在架构上,
\(\mathcal{E}_\text{vision, vis-only}\)

\(\mathcal{E}_\text{vision, vis-lang}\)
,以及
\(\mathcal{D}_\text{vis-only}\)

\(\mathcal{D}_\text{vis-lang}\)
是相当相似的,区别在于
\(\mathcal{E}_\text{vision, vis-lang}\)
在预训练后保持不变,仅对视觉-语言解码器
\(\mathcal{D}_\text{vis-lang}\)
进行微调。通过这种方式,将零样本
CLIP
在图像级别的竞争性
OOD
检测性能转移到像素级任务中。

Max-Logit
提示集成于类合并

优化
ID
类文本嵌入可以使其更好地与相应的
ID
视觉嵌入对齐,提高
ID

OOD
类别之间的可分离性,但盲目地微调文本编码器可能导致灾难性遗忘。

为此,论文通过
max-logit
提示集成在文本提示中引入概念词汇多样性和具体化,显著提高模型对
OOD
输入的敏感性。词汇多样性包括同义词和复数形式,而具体化涉及更好地与
CLIP
预训练对齐的分解概念。例如,使用概念{
vegetation
,
tree
,
trees
,
palm tree
,
bushes
}来表示类
vegetation

max-logit
集成考虑给定类
\(k\)
的所有替代概念,替换内容为视觉嵌入
\(\mathbf{v}_i\)
与所有
\(l\)
个替代文本嵌入
\([\mathbf{t}_{k}^{1}, \ldots, \mathbf{t}_{k}^{l}]\)
的最大余弦相似度:

\[\begin{equation}
\max\Big(
\begin{bmatrix}
\text{cos}(\mathbf{v}_i, \mathbf{t}_{k}^{1}), &
\text{cos}(\mathbf{v}_i, \mathbf{t}_{k}^{2}), &
\ldots, &
\text{cos}(\mathbf{v}_i, \mathbf{t}_{k}^{l})
\end{bmatrix}\Big).
\end{equation}
\]

此外,单靠在
\(K\)
类维度上的最大像素级得分可能导致次优性能,因为在两个
ID
类之间的边缘像素的不确定性较高,尤其是当类别数量增加时。

为了解决这个问题,将相关的
ID
类合并为超类。通过在测试期间将各个语义类的文本提示作为不同的替代概念连接到超类中来实现,而无需重新训练。然后,可以使用
max-logit
方法获得超类的不确定性。

通过
OOD
提示实现无数据、无训练异常监督

通过视觉-语言预训练,通常能够很好地检测到与
ID
类不同的语义
OOD
类(远
OOD
类)。但当
OOD
类与
ID
类非常相似的情况(近
OOD
类),则更具挑战性。例如,在
CityScapes
类别中,
OOD
类大篷车在城市驾驶场景中可能在视觉上与
ID
类卡车相似。

利用视觉-语言模型的开放词汇能力,论文引入了一种新的评分函数,旨在更好地检测这些近
OOD
类,而不需要额外的训练或数据准备。

为了在测试时整合
\(Q\)
个新的
OOD
概念,需要通过
\(Q\)
个额外的项
\(\text{cos}(\mathbf{v}_i, \mathbf{t}_{K+1}), \ldots, \text{cos}(\mathbf{v}_i, \mathbf{t}_{K+Q})\)
扩展公式
1
中的掩码分类得分
\(\mathbf{c}_i\)
。遵循公式
2
,即通过将
\(\mathbf{c} \in \left[0, 1\right]^{N\times (K+Q)}\)
的前
\(K\)
个通道与掩码预测得分
\(\mathbf{s} \in \left[0, 1\right]^{N\times H\times W}\)
进行组合,获得最终的不确定性得分
\(\mathbf{u} \in \mathbb{R}^{H\times W}\)

\[\begin{equation}
\mathbf{u}_{h,w} = -\max_{k}\sum_{i=1}^{N} \mathbf{s}_{i, h, w} \cdot \mathbf{c}_{i, k}\ \ .
\end{equation}
\]

通过这一整合,
\(Q\)
类中的
OOD
对象将(在大多数情况下)正确分配到其相应的类别。如果没有这一整合,它们可能会被错误地分配到与其实际
OOD
类别相似的
ID
类。相反,如果输入中不存在
OOD
对象,额外的
\(Q\)
类的影响将保持微不足道。

主要实验




如果本文对你有帮助,麻烦点个赞或在看呗~
更多内容请关注 微信公众号【晓飞的算法工程笔记】

work-life balance.

前言

Redis在我们的日常开发工作中,使用频率非常高,已经变成了必不可少的技术之一。

Redis的使用场景也很多。

比如:保存用户登录态,做限流,做分布式锁,做缓存提升数据访问速度等等。

那么问题来了,Redis的性能要如何优化?

为了充分发挥Redis的性能,这篇文章跟大家一起聊聊Redis性能优化的18招,希望对你会有所帮助。

1. 选择合适的数据结构

Redis支持多种数据结构,如字符串、哈希、列表、集合和有序集合。根据实际需求选择合适的数据结构可以提高性能。

如果要存储用户信息,考虑使用哈希而不是多个字符串:

jedis.hset("user:1001", "name", "Alice");
jedis.hset("user:1001", "age", "30");

这样可以高效地存储和访问多个属性。

2. 避免使用过大的key和value

较长的key和value会占用更多内存,还可能影响性能。

保持key简短,并使用简洁的命名约定。

比如:
将“user:1001:profile”简化为“u:1001:p”。

还可以做压缩等其他优化。

如果对大key问题,比较感兴趣可以看看我的另一篇文章《
从2s优化到0.1s,我用了这5步
》,里面有非常详细的介绍。

3. 使用Redis Pipeline

对多个命令的批量操作,使用Pipeline可以显著降低网络延迟,提升性能。

比如,批量设置key可以这样做:

Pipeline p = jedis.pipelined();
for (int i = 0; i < 1000; i++) {
    p.set("key:" + i, "value:" + i);
}
p.sync();

这样一次性可以发送多个命令,减少了网络往返时间,能够提升性能。

4. 控制连接数量

过多的连接会造成资源浪费,使用
连接池
可以有效管理连接数量。

比如,使用JedisPool:

JedisPool pool = new JedisPool("localhost");
try (Jedis jedis = pool.getResource()) {
    jedis.set("key", "value");
}

有了连接池,这样连接就会被复用,而不是每次都创建新连接,使用完之后,又放回连接池。

能有效的节省连接的创建和销毁时间。

5. 合理使用过期策略

设置合理的过期策略,能防止内存被不再使用的数据占满。

例如,缓存热点数据可以设置过期时间。

比如,对会话数据设置过期时间:

jedis.setex("session:12345", 3600, "data");

Redis内部会定期清理过期的缓存。

6. 使用Redis集群

数据量增大时,使用Redis集群可以将数据分散到多个节点,提升并发性能。

可以将数据哈希分片到多个Redis实例。

这样可以避免单个Redis实例,数据太多,占用内存过多的问题。

7. 充分利用内存优化

选择合适的内存管理策略,Redis支持LRU(Least Recently Used)策略,可以自动删除不常用的数据。

比如,配置Redis的maxmemory:

maxmemory 256mb
maxmemory-policy allkeys-lru

8. 使用Lua脚本

Lua脚本让多条命令在Redis中原子性执行,减少网络延迟。

比如,使用Lua防止多个命令的网络延迟:

EVAL "redis.call('set', KEYS[1], ARGV[1]) return redis.call('get', KEYS[1])" 1 "key" "value"

使用Lua脚本,可以保证Redis的多个命令是原子性操作。

9. 监控与调优

使用INFO命令监控Redis性能数据,如命令支持、内存使用等,及时调优。

比如,使用命令获取监控信息:

INFO memory
INFO clients

10. 避免热点key

热点key会造成单一节点的压力,通过随机化访问来避免。

比如,可以为热点key加随机后缀:

String key = "hotkey:" + (System.currentTimeMillis() % 10);
jedis.incr(key);

11. 使用压缩

存储大对象时,考虑使用压缩技术来节省内存。

比如,可以使用
GZIP
压缩JSON数据:

byte[] compressed = gzipCompress(jsonString);
jedis.set("data", compressed);

12. 使用Geo位置功能

Redis支持地理位置存储和查询,使用
GEOADD
可以高效管理地理数据。

比如,存储地点信息:

jedis.geoadd("locations", longitude, latitude, "LocationName");

13. 控制数据的持久化

合理设置
RDB

AOF
的持久化策略,避免频繁写盘造成性能下降。

示例:
设置持久化的时间间隔:

save 900 1
appendonly yes

14. 尽量减少事务使用

在高并发场景下,避免过度使用MULTI/EXEC,因为事务会锁住key。

可以直接使用单条命令替代事务。

15. 合理配置客户端

调整客户端的连接超时和重连策略,以适应高负载场景,确保连接稳定。

例如:

JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(128); // 最大连接数
poolConfig.setMaxIdle(64); // 最大空闲连接
poolConfig.setMinIdle(16); // 最小空闲连接
poolConfig.setTestOnBorrow(true);
poolConfig.setTestOnReturn(true);
poolConfig.setTestWhileIdle(true);

JedisPool jedisPool = new JedisPool(poolConfig, "localhost", 6379, 2000); // 连接超时2000ms

16. 使用Redis Sentinel

使用
Sentinel
进行监控,实现高可用性,确保系统在故障时能够快速切换。

配置Sentinel进行主从复制。

17. 优化网络配置

保证Redis服务器有良好的网络带宽,避免网络瓶颈。

使用服务器内部专线,减少延迟。

18. 定期清理不必要的数据

生命周期管理很关键,定期删除过期或不必要的数据,保持内存高效利用。

可以设置
Cron
任务定期清理。

虽说Redis内部会清理过期的数据,但有些长期存在的垃圾数据,也建议及时清理。

总结

以上就是Redis性能优化的18条军规,灵活应用这些策略能够为你的项目带来显著的性能提升。希望能帮助到你,欢迎分享你的优化经验!

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。
求一键三连:点赞、转发、在看。
关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。

技术背景

在前面的一篇
博客
中我们介绍了MindSpore-2.4-gpu的安装和其中可能出现的一些问题。这里我们在安装完成之后,可以尝试使用一些MindSpore新版本的特性。那么在安装之后,如果是使用VSCode作为IDE,可以使用ctrl+shift+P快捷键,然后搜索
python:sele
将Python解释器切换到我们所需要的最新MindSpore环境下。

设备管理和资源监测

在mindspore-2.4版本中增加了
mindspore.hal
接口,可以用于管理设备、监测设备以及执行流的处理等等。例如,常用的获取设备的数量:

import mindspore as ms
ms.set_context(device_target="GPU")
device_target = ms.context.get_context("device_target")
print(ms.hal.device_count(device_target))
# 2

这个输出表明我们的环境下有两个GPU卡。也可以打印这两块显卡的名称:

import mindspore as ms
ms.set_context(device_target="GPU")
device_target = ms.context.get_context("device_target")
print(ms.hal.get_device_name(0, device_target))
print(ms.hal.get_device_name(1, device_target))
# Quadro RTX 4000
# Quadro RTX 4000

以及设备的可用状态:

import mindspore as ms
ms.set_context(device_target="GPU")
device_target = ms.context.get_context("device_target")
print(ms.hal.is_available(device_target))
# True

查询设备是否被初始化:

import mindspore as ms
ms.set_context(device_target="GPU")
device_target = ms.context.get_context("device_target")
print(ms.hal.is_initialized(device_target))
A = ms.Tensor([0.], ms.float32)
A2 = (A+A).asnumpy()
print(ms.hal.is_initialized(device_target))
# False
# True

这也说明,只有在计算的过程中,MindSpore才会将Tensor的数据传输到计算后端。除了设备管理之外,新版本的MindSpore还支持了一些内存监测的功能,对于性能管理非常的实用:

import mindspore as ms
import numpy as np
ms.set_context(device_target="GPU")
A = ms.Tensor(np.random.random(1000), ms.float32)
A2 = (A+A).asnumpy()
print(ms.hal.max_memory_allocated())
# 8192

这里输出的占用最大显存的Tensor的大小。需要说明的是,这里不能直接按照浮点数占用空间来进行计算,应该说MindSpore在构建图的过程中会产生一些额外的数据结构,这些数据结构也会占用一定的显存,但是显存增长的趋势是准确的。除了单个的打印,还可以整个的输出一个summary:

import mindspore as ms
import numpy as np
ms.set_context(device_target="GPU")
A = ms.Tensor(np.random.random(1000), ms.float32)
A2 = (A+A).asnumpy()
print(ms.hal.memory_summary())

输出的结果为:

|=============================================|
|               Memory summary                |
|=============================================|
| Metric               | Data                 |
|---------------------------------------------|
| Reserved memory      |   1024 MB            |
|---------------------------------------------|
| Allocated memory     |   4096 B             |
|---------------------------------------------|
| Idle memory          |   1023 MB            |
|---------------------------------------------|
| Eager free memory    |      0 B             |
|---------------------------------------------|
| Max reserved memory  |   1024 MB            |
|---------------------------------------------|
| Max allocated memory |   8192 B             |
|=============================================|

ForiLoop

其实简单来说就是一个内置的for循环的操作,类似于Jax中的fori_loop:

import mindspore as ms
import numpy as np
from mindspore import ops
ms.set_context(device_target="GPU")

@ms.jit
def f(_, x):
    return x + x

A = ms.Tensor(np.ones(10), ms.float32)
N = 3
AN = ops.ForiLoop()(0, N, f, A).asnumpy()
print (AN)
# [8. 8. 8. 8. 8. 8. 8. 8. 8. 8.]

有了这个新的for循环体,我们可以对整个循环体做端到端自动微分:

import mindspore as ms
import numpy as np
from mindspore import ops, grad
ms.set_context(device_target="GPU", mode=ms.GRAPH_MODE)

@ms.jit
def f(_, x):
    return x + x

@ms.jit
def s(x, N):
    return ops.ForiLoop()(0, N, f, x)

A = ms.Tensor(np.ones(10), ms.float32)
N = 3
AN = grad(s, grad_position=(0, ))(A, N).asnumpy()
print (AN)
# [8. 8. 8. 8. 8. 8. 8. 8. 8. 8.]

流计算

CUDA Stream流计算是CUDA高性能编程中必然会用到的一个特性,其性能优化点来自于数据传输和浮点数运算的分离,可以做到在不同的Stream中传输数据,这样就达到了一边传输数据一边计算的效果。相比于单个Stream的
传输-计算-等待-传输-计算
这样的模式肯定是要更快一些,而有些深度学习框架其实很早就已经支持了Stream的调度,MindSpore目前也是跟上了节奏。关于Stream计算适用的一些场景,首先我们来看这样一个例子:

import mindspore as ms
import numpy as np
np.random.seed(0)
from mindspore import numpy as msnp
ms.set_context(device_target="GPU", mode=ms.GRAPH_MODE)

@ms.jit
def U(x, mu=1.0, k=1.0):
    return msnp.sum(0.5 * k * (x-mu) ** 2)

x = ms.Tensor(np.ones(1000000000), ms.float32)
energy = U(x)
print (energy)

在本地环境下执行就会报错:

Traceback (most recent call last):
  File "/home/dechin/projects/gitee/dechin/tests/test_ms.py", line 13, in <module>
    energy = U(x)
  File "/home/dechin/anaconda3/envs/mindspore-master/lib/python3.9/site-packages/mindspore/common/api.py", line 960, in staging_specialize
    out = _MindsporeFunctionExecutor(func, hash_obj, dyn_args, process_obj, jit_config)(*args, **kwargs)
  File "/home/dechin/anaconda3/envs/mindspore-master/lib/python3.9/site-packages/mindspore/common/api.py", line 188, in wrapper
    results = fn(*arg, **kwargs)
  File "/home/dechin/anaconda3/envs/mindspore-master/lib/python3.9/site-packages/mindspore/common/api.py", line 588, in __call__
    output = self._graph_executor(tuple(new_inputs), phase)
RuntimeError: 
----------------------------------------------------
- Memory not enough:
----------------------------------------------------
Device(id:0) memory isn't enough and alloc failed, kernel name: 0_Default/Sub-op0, alloc size: 4000000000B.

----------------------------------------------------
- C++ Call Stack: (For framework developers)
----------------------------------------------------
mindspore/ccsrc/runtime/graph_scheduler/graph_scheduler.cc:1066 Run

说明出现了内存不足的情况。通常情况下,可能需要手动做一个拆分,然后使用循环体遍历:

import time
import mindspore as ms
import numpy as np
from mindspore import numpy as msnp
ms.set_context(device_target="GPU", mode=ms.GRAPH_MODE)

@ms.jit
def U(x, mu=1.0, k=1.0):
    return msnp.sum(0.5 * k * (x-mu) ** 2)

def f(x, N=1000, size=1000000):
    ene = 0.
    start_time = time.time()
    for i in range(N):
        x_tensor = ms.Tensor(x[i*size:(i+1)*size], ms.float32)
        ene += U(x_tensor)
    end_time = time.time()
    print ("The calculation time cost is: {:.3f} s".format(end_time - start_time))
    return ene.asnumpy()

x = np.ones(1000000000)
energy = f(x)
print (energy)
# The calculation time cost is: 11.732 s
# 0.0

这里至少没有报内存错误了,因为每次只有在计算的时候我们才把相应的部分拷贝到显存中。接下来使用流计算,也就是边拷贝边计算的功能:

def f_stream(x, N=1000, size=1000000):
    ene = 0.
    s1 = ms.hal.Stream()
    s2 = ms.hal.Stream()
    start_time = time.time()
    for i in range(N):
        if i % 2 == 0:
            with ms.hal.StreamCtx(s1):
                x_tensor = ms.Tensor(x[i*size:(i+1)*size], ms.float32)
                ene += U(x_tensor)
        else:
            with ms.hal.StreamCtx(s2):
                x_tensor = ms.Tensor(x[i*size:(i+1)*size], ms.float32)
                ene += U(x_tensor)
    ms.hal.synchronize()
    end_time = time.time()
    print ("The calculation with stream time cost is: {:.3f} s".format(end_time - start_time))
    return ene.asnumpy()

因为要考虑到程序编译对性能带来的影响,所以这里使用与不使用Stream的对比需要分开执行。经过多次测试之后,不使用Stream的运行时长大约为:

The calculation time cost is: 10.925 s
41666410.0

而使用Stream的运行时长大约为:

The calculation with stream time cost is: 9.929 s
41666410.0

就直观而言,Stream计算在MindSpore中有可能带来一定的加速效果,但其实这种加速效果相比于直接写CUDA Stream带来的效果增益其实要弱一些,可能跟编译的逻辑有关系。但至少现在有了Stream这样的一个工具可以在MindSpore中直接调用,就可以跟很多同类型的框架同步竞争了。

总结概要

接上一篇对于MindSpore-2.4-gpu版本的安装介绍,本文主要介绍一些MindSpore-2.4版本中的新特性,例如使用hal对设备和流进行管理,进而支持Stream流计算。另外还有类似于Jax中的fori_loop方法,MindSpore最新版本中也支持了ForiLoop循环体,使得循环的执行更加高效,也是端到端自动微分的强大利器之一。

版权声明

本文首发链接为:
https://www.cnblogs.com/dechinphy/p/ms24.html

作者ID:DechinPhy

更多原著文章:
https://www.cnblogs.com/dechinphy/

请博主喝咖啡:
https://www.cnblogs.com/dechinphy/gallery/image/379634.html