2024年10月

一个优秀的数据应用不仅仅是功能的强大,更在于其用户体验的打造。

而良好的
页面布局
,作为用户体验的重要组成部分,不仅能够提升信息的可读性,还能引导用户高效地完成操作。

反之,混乱的布局会让人感到困惑和挫败,甚至导致用户放弃使用应用。


Streamlit
中,
Sidebar
(侧边栏)、
Columns
(列布局)和
Tabs
(标签页)是三个至关重要的布局组件,它们各自有不同的使用场景,共同构成了应用界面的骨架。

1. 侧边栏 st.sidebar

Sidebar
(侧边栏)在
Streamlit
应用中主要是导航作用。

Sidebar
通常位于应用界面的左侧或右侧,它的优势在于,既不会占用主内容区域的空间,又同时保持了对关键功能的即时访问,特别适用于那些需要频繁切换功能或具有多层导航结构的应用。

Sidebar
不是菜单,一般不用于页面的切换,它的里面可以包含其他的组件。

下面构建一个简化的数据分析场景,演示
Sidebar
的使用方法。

我们在
Sidebar
中设置需要选择的数据集和数据分类(分为
测试集

验证集
),

然后在主页面中获取选择的数据集和数据分类,只实现操作数据的功能。

# 侧边栏中选择数据集
dataset = st.sidebar.selectbox(
    "选择数据集?",
    (
        "手写数字数据",
        "房屋成交数据",
        "股市交易数据",
    ),
)

# 侧边栏中选择数据分类
data_type = st.sidebar.radio(
    "数据分类: ",
    ("测试集", "验证集", "所有数据"),
)

# 右侧页面中的模拟操作
st.title(f"数据集: {dataset}")
st.divider()
st.write(f"数据分类: 【{data_type}】")
st.write(f"TODO!!!: 操作数据的功能")

2. 列布局 st.columns

Columns
布局,即列布局,是一种将页面内容分割成多个垂直排列的列,以便更有效地展示信息的布局方式。

这种布局方式不仅提升了页面的视觉吸引力,还显著增强了用户体验,使得信息获取更加直观和便捷。


Streamlit
中,可以使用
st.columns
函数来创建列布局。

这个函数接受一个列表作为参数,列表中的每个元素都是一个函数,代表一列的内容。

Streamlit
会按照列表的顺序和长度来渲染列。

比如,我们构造一个数据分析的示例,分3列展示内容,

  • 第一列模拟选择数据
  • 第二列模拟一些配置信息
  • 第三列根据配置模拟绘制图形
# 绘图的类型放在session中
if "graph" not in st.session_state:
    st.session_state.graph = ""

# 第一列
def column_1():
    st.header("1. 选择数据")
    st.selectbox(
        "选择数据集?",
        (
            "手写数字数据",
            "房屋成交数据",
            "股市交易数据",
        ),
    )
    # 随机模拟的数据
    data = pd.DataFrame(np.random.randn(5, 3), columns=["A", "B", "C"])
    st.table(data)


def column_2():
    st.header("2. 配置数据")
    graph = st.radio(
        "图形类型: ",
        ("折线图", "柱状图", "散点图"),
    )

    st.session_state.graph = graph


def column_3():
    st.header("3. 绘制图形")

    chart_data = pd.DataFrame(np.random.randn(20, 3), columns=["A", "B", "C"])
    if st.session_state.graph == "散点图":
        st.scatter_chart(chart_data)

    if st.session_state.graph == "折线图":
        st.line_chart(chart_data)

    if st.session_state.graph == "柱状图":
        st.bar_chart(chart_data)


col1, col2, col3 = st.columns(3)

with col1:
    column_1()

with col2:
    column_2()

with col3:
    column_3()

3. 标签页 st.tabs

Tabs
组件的主要用途在于将复杂的信息结构分解为多个独立的、可切换的部分,每个部分(或称为“标签页”)都包含相关的内容或功能。

这种设计方式有助于减少用户在同一时间内需要处理的信息量,避免界面过载,同时也便于用户快速找到所需内容。

同样是上面的例子,如果数据的字段很多,配置数据的项目也很多,那么上面的
Columns
布局就显得很拥挤,

这时,用
Tabs
布局更好,代码调整很简单。

tab1, tab2, tab3 = st.tabs(["1. 选择数据", "2. 配置数据", "3. 绘制图形"])

with tab1:
    tab_1()

with tab2:
    tab_2()

with tab3:
    tab_3()

上面函数
tab_1()

tab_2()

tab_3()
中的代码和
Columns
布局示例中的
column_1()

column_2()

column_3()
代码基本一样。

随着
Columns
布局的每个
Columns
内容逐渐增多时,可以考虑改用
Tabs
布局。

4. 总结

总之,这三种布局方式各有千秋,选择哪种布局取决于应用的具体需求、用户习惯以及设计目标。

Sidebar
(侧边栏)提供持续可见的导航菜单,适合功能繁多或需频繁切换的应用,帮助用户快速定位所需功能,同时保持主内容区域的清晰。

Columns
(列布局)则通过分割界面为多个并列区域,实现图文混排或数据报表的灵活展示。

它提高了信息密度,使得用户能够在同一视图中获取更多信息,适用于需要同时展示多种数据类型或视图的场景。

Tabs
(标签页)将内容划分为多个独立的、可切换的部分,每个部分包含相关功能或信息。

它有助于节省屏幕空间,同时帮助用户对信息进行逻辑分组,提高信息检索效率。

Tabs
适用于功能或信息分类明确,且用户可能需要根据不同需求切换查看的应用。

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

论文: Straightforward Layer-wise Pruning for More Efficient Visual Adaptation

创新点


  • 提出了一种针对
    PETL
    模型的剪枝方法
    SLS

    Straightforward Layer-wiSe Pruning method
    ),证明在下游数据集与预训练数据集之间存在显著差距时,
    PETL
    转移后的模型中存在大量冗余参数。
  • 提出了一种直观的特征级分析方法,为评估结构剪枝参数的重要性提供了一种新的视角。
  • SLS

    VTAB-1k
    基准测试中,使用相同的剪枝参数数量,以简单的策略在模型存储、准确性和速度上超越了当前主流的结构剪枝方法
    DepGraph

内容概述


参数高效迁移学习(
PETL
)旨在使用有限的参数调整大型预训练模型。虽然大多数
PETL
方法在训练过程中仅更新添加或选择的参数并冻结预训练权重,但因为
PETL
难以回传梯度调整顶层的参数,往往会导致冗余的模型结构。结构剪枝有效降低了模型冗余,但常见的剪枝方法通常会导致存储参数的过度增加,因为不同剪枝率和数据会产生不同的剪枝结构。

针对存储参数量的问题,论文提出了一种直接的层级剪枝方法
SLS

Straightforward Layer-wiSe Pruning method
),用于剪枝
PETL
模型。
SLS
通过从特征角度评估每一层的参数,并利用聚类度量通过
t-SNE
获得的低维空间中的聚类现象来评估当前层参数,
SLS
促进了基于信息的剪枝决策。逐层剪枝专注于存储剪枝索引,解决了存储量的问题。

值得注意的是,主流的逐层剪枝方法可能不适合评估
PETL
转移模型中层的重要性,因为大多数参数是预训练的,并且与下游数据集的相关性有限。与最先进的
PETL
方法进行的比较分析表明,剪枝模型在模型吞吐量和准确性之间实现了显著的平衡。此外,
SLS
有效减少了由于不同剪枝结构而产生的存储开销,同时在与传统剪枝方法相比时提升了剪枝模型的准确性和速度。

SLS


以往的研究表明,较低层通常捕捉一般特征,而较高层则专注于特定特征。基于这一理解以及在
PETL
模型中冻结预训练网络参数的局限性,论文认为当下游数据分布与预训练数据分布之间存在显著差异时,
PETL
转移的模型深层将包含大量冗余参数。于是论文打算动态识别并剪除这些冗余参数。为了确保剪枝过程的有效性,必须满足以下关键标准:

  1. 保持与
    PETL
    转移方法相同的存储参数数量。

  2. 预测冗余参数而无需额外的训练。

相关算法

  • 降维算法
    t-SNE
    :给定一组
    \(d\)
    维输入特征
    \(X=\{x_1,x_2,...,x_n\}\in \mathbb{R}^{n\times d}\)
    ,为
    \(X\)
    计算一组
    \(s\)
    维嵌入,记作
    \(Y=\{y_1,y_2,...,y_n\}\in \mathbb{R}^{n\times s}\)
    。其中
    \(s\ll d\)
    ,通常为
    2

    3
    以便于可视化。首先使用联合概率来度量输入
    \(X\)

    \(x_i\)

    \(x_j\)
    之间的相似性,随后调整随机初始化
    \(Y\)
    ,使
    \(Y\)
    元素之间的相似性与
    \(X\)
    对应元素之间的相似性一致。

  • 聚类算法指标
    SC_Index
    (
    Silhouette Coefficient Index
    ):给定一组聚类结果
    \(X=\{x_1,x_2,...,x_n\}\)
    ,对于某一点
    \(x_i\)
    ,定义
    \(a(i)\)
    为其所在聚类中剩余点与
    \(x_i\)
    之间的平均距离,
    \(b(i)\)

    \(x_i\)
    与最近聚类中所有点之间的平均距离。


    \[\begin{equation}
    \bar{s}=\frac{1}{n}\sum_{i=1}^n\frac{b(i)-a(i)}{\max(a(i),b(i))}
    \end{equation}
    \]

层级剪枝不会增加存储参数数量

SLS
的预测不涉及额外的参数引用,采用基于现有特征的聚类方法预测剪枝层索引表示为
\(Index_j\)
直接进行剪枝。因此,通过
SLS
剪枝的模型不会产生任何额外的存储开销。

根据各层的中间特征做出剪枝决定

使用降维特征的聚类程度
SC_Index
来评估层特征,该方法不引入额外的监督训练。如图
2
所示,在适当的设置下,模型中当前层的分类准确率与降维特征的
SC_Index
之间存在明显的相关性。

论文提出了特征评估模块 (
FEM
) 来评估来自层
\(L_i\)
的特征。如图
3
所示,
FEM
从层
\(L_i\)
的输出
\([x_i,e_i]\)
中提取
cls_token
\(x_i\)
来表示当前特征,使用
t-SNE
算法将
\(x_i\)
降维到
\(x_i' \in \mathbb{R}^{B\times2}\)
。随后,通过结合对应于当前输入的标签
\(\in \mathbb{R}^{B\times1}\)
,得到一个具有
\(p\)
个类别的聚类结果
\(C\)
,其中
\(p\)
是当前数据集中的类别数量。最后,计算与
\(C\)
对应的值
\(a(i)\)

\(b(i)\)
,确定当前层特征的评估值
\(SC\_index_i\)

对于一个具有
\(N\)
层的模型,设
\(\mathbf{\alpha}\)
为一个超参数,控制
SLS
剪枝的程度。当前数据集上剪枝层数的阈值
\(T\)
定义为

\[\begin{equation}
\label{eq11}
T=\mathbf{\alpha} \times SC\_Index_N
\end{equation}
\]

在模型的剪枝过程中,从最高层向下遍历。当第
\(i^{th}\)
层特征的评估值
\(SC\_Index_i\)
低于阈值
\(T\)
时,停止遍历循环,并剪掉从
\(i+2\)

\(N\)
的层。这一设计的动机在于,当第
\(i^{th}\)
层特征的评估值低于与顶部层特征评估相比的某个阈值时,分类头将无法有效地区分当前特征。因此,
\({i+1}^{th}\)
层的输出特征是分类头能够很好地区分的最低层,剪掉从
\(i+1\)
向下的层会对模型的性能产生显著影响。

主要实验结果




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

work-life balance.

1 什么是本地方法

首先要知道什么是本地方法,本地方法并不是 JVM 自己的方法,也不是 jre 里面的方法,而是指那些操作系统自己的方法(如C/C++方法),它们在操作系统目录里。可以这么理解,本地方法就是计算机操作系统对外提供的方法,JVM 通过调用这些方法可以实现 Java 程序和计算机的交互。

1.1 本地方法的好处

  1. 访问操作系统资源
    :直接调用操作系统的API,例如文件系统、网络接口、图形用户界面等。
  2. 性能优化
    :对于某些计算密集型任务,使用C或C++等语言实现可以显著提高性能。
  3. 使用现有库
    :利用已经存在的C/C++库,避免重复开发和维护。
  4. 硬件访问
    :直接访问硬件设备,例如摄像头、传感器等。

所以如果我们想优化计算密集型任务的性能,或是调用 Java 中没有实现的计算机功能,我们可以自己实现一个本地方法。

1.2 声明本地方法

在Java中,本地方法通过
native
关键字声明。例如:

public class MyClass {
    // 声明本地方法
    public native void nativeMethod();

    // 静态块中加载本地库
    static {
        System.loadLibrary("mylib"); // 加载名为mylib的本地库
    }

    public static void main(String[] args) {
        new MyClass().nativeMethod(); // 调用本地方法
    }
}

1.3 实现本地方法

本地方法的实现通常使用JNI(Java Native Interface)或JNA(Java Native Access)来完成。以下是使用JNI实现本地方法的步骤:

1. 生成头文件

使用
javah
工具生成包含本地方法签名的C头文件。假设上面的Java类保存为
MyClass.java
,编译后生成
MyClass.class
,然后运行:

javah -jni MyClass

这将生成一个名为
MyClass.h
的头文件,内容如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class MyClass */

#ifndef _Included_MyClass
#define _Included_MyClass
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     MyClass
 * Method:    nativeMethod
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_MyClass_nativeMethod
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

2. 编写C语言实现

根据生成的头文件,编写C语言实现。例如:

#include <jni.h>
#include <stdio.h>
#include "MyClass.h"

// 实现本地方法
JNIEXPORT void JNICALL Java_MyClass_nativeMethod(JNIEnv *env, jobject obj) {
    printf("Hello from native method!\n");
}

3. 编译C代码

将C代码编译成动态链接库。假设C文件名为
mylib.c
,编译命令如下:

在Linux上:

gcc -shared -o libmylib.so -I/usr/lib/jvm/java-8-openjdk-amd64/include -I/usr/lib/jvm/java-8-openjdk-amd64/include/linux mylib.c

在Windows上:

cl -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -LD mylib.c -Fe mylib.dll

4. 运行Java程序

确保动态链接库在Java程序的库路径中,然后运行Java程序:

java -Djava.library.path=. MyClass

1.4 使用JNA

JNA是一种更简单的方式来调用本地库,不需要编写C代码。以下是一个使用JNA的示例:

  1. 添加JNA依赖
    :在项目中添加JNA的依赖。如果你使用Maven,可以在
    pom.xml
    中添加:
<dependency>
    <groupId>net.java.dev.jna</groupId>
    <artifactId>jna</artifactId>
    <version>5.8.0</version>
</dependency>
  1. 定义接口
    :定义一个接口来映射本地库中的函数。
import com.sun.jna.Library;
import com.sun.jna.Native;

public interface MyLib extends Library {
    MyLib INSTANCE = Native.load("mylib", MyLib.class);

    void nativeMethod();
}
  1. 调用本地方法
public class MyClass {
    public static void main(String[] args) {
        MyLib.INSTANCE.nativeMethod(); // 调用本地方法
    }
}

1.5 总结

本地方法是Java程序中的一种特殊方法,其声明在Java代码中,但实现由非Java语言编写。通过本地方法,Java程序可以访问操作系统资源、优化性能、使用现有库和直接访问硬件设备。常见的实现方式包括JNI和JNA。

2 本地方法栈

2.1 特点

  1. 线程私有
    :每个线程都有自己的本地方法栈,与Java虚拟机栈一样,本地方法栈也是线程私有的。
  2. 存储结构
    :本地方法栈中的每个栈帧(Frame)对应一次本地方法的调用。栈帧中包含本地方法的参数、局部变量、操作数栈等信息。
  3. 调用机制
    :当Java代码调用一个本地方法时,JVM会创建一个新的栈帧并将其压入本地方法栈。本地方法执行完毕后,栈帧会被弹出并丢弃。

2.2 本地方法栈与Java虚拟机栈的区别

  • 用途不同
    :Java虚拟机栈用于支持Java方法的执行,而本地方法栈用于支持本地方法的执行。
  • 实现方式
    :Java虚拟机栈的实现由JVM规范规定,而本地方法栈的实现通常依赖于具体的JVM实现和操作系统的ABI(Application Binary Interface)。
  • 数据类型
    :Java虚拟机栈主要处理Java类型的值,而本地方法栈可能涉及更广泛的C/C++类型或其他原生类型。

2.3 本地方法栈的工作流程

  1. 方法调用
    :当Java代码中调用一个声明为
    native
    的方法时,JVM会查找该方法的本地实现。
  2. 栈帧创建
    :JVM为本地方法创建一个新的栈帧,并将其压入本地方法栈。
  3. 参数传递
    :调用本地方法所需的参数会被从Java虚拟机栈复制到本地方法栈的栈帧中。
  4. 方法执行
    :本地方法在本地方法栈中执行,可以访问操作系统资源、硬件设备等。
  5. 结果返回
    :本地方法执行完毕后,结果会被从本地方法栈复制回Java虚拟机栈,然后继续执行Java代码。
  6. 栈帧弹出
    :本地方法栈中的栈帧被弹出并丢弃。

2.4 总结

本地方法栈是JVM中用于支持本地方法调用的重要数据结构。通过本地方法栈,Java程序可以调用用其他语言编写的代码,从而实现更广泛的功能和更高的性能。

Manim

Axes
对象是通用的坐标系对象,之前几篇介绍的
数轴
和各种
坐标平面
都是继承
Axes
对象。

Axes
对象的主要作用在于创建和管理二维坐标轴,以满足我们制作数学动画时的各种需求。

具体来说,
Axes
对象可以帮助我们:

  1. 定义坐标系
    :定义一个明确的坐标系,通过设置x轴和y轴的范围、步长等参数,创建符合自己需求的坐标轴
  2. 定制坐标轴样式
    :可以设置坐标轴的颜色、刻度线的长度、箭头的长度等属性,还可以控制是否显示刻度线、标签和注释等
  3. 绘制函数曲线
    :绘制各种自定义的函数曲线,如二次函数、三角函数、指数函数等
  4. 添加图形和标签
    :可以添加图形元素(如圆、三角形等)和标签(如坐标轴标签、标题等)

1. 主要参数

Axes
的主要参数有:

参数名称 类型 说明
x_range Sequence[float] 横坐标轴
范围,间隔
y_range Sequence[float] 纵坐标轴
范围,间隔
x_length float 横轴
的宽度
y_length float 纵轴
的高度
axis_config dict 坐标轴的通用配置选项
x_axis_config dict 横轴
的配置选项
y_axis_config dict 纵轴
的配置选项
tips bool 横轴
的右边和
纵轴
上边是否显示箭头

参数
axis_config

x_axis_config

y_axis_config
中的可配置的项目类似,

区别在于
axis_config
是针对整个坐标轴的配置,

x_axis_config

y_axis_config
是分别针对横轴和纵轴的配置。

如果它们设置了相同的项,那么
x_axis_config

y_axis_config
中的设置会覆盖axis_config中相应的设置。

可对坐标轴设置的项有很多,下面列举了一些常用的。

  • stroke_color
    :设置坐标轴线的颜色
  • stroke_width
    (或
    line_width
    ):设置坐标轴线的宽度
  • include_numbers
    :是否在坐标轴上显示数字
  • numbers_to_include
    :指定要显示数字的列表。
  • numbers_with_elongated_ticks
    :指定要加长刻度线的数字列表。用于突出显示特定的刻度
  • include_ticks
    :是否显示刻度线
  • tick_size
    :设置刻度线的长度
  • include_tip
    :是否在坐标轴的末端显示箭头
  • tip_length
    :设置箭头的长度。当
    include_tip

    True
    时,此选项才有效
  • font_size
    :设置坐标轴上数字的字体大小
  • label_direction
    :设置坐标轴标签的方向
  • scaling
    :设置坐标轴的刻度缩放方式,如线性刻度、对数刻度等

2. 主要方法

Axes
的方法主要是为了动态获取坐标轴上的各种信息,以及在坐标轴上绘制函数图形。

其中用的比较多的有:

名称 说明
coords_to_point 将坐标转换为在屏幕上点的位置
point_to_coords 将屏幕上点的位置转换为坐标
get_axes 获取各个坐标轴的信息
plot_line_graph 在坐标轴中绘制函数图形

3. 使用示例

下面通过由简单到复杂的示例,一步步演示如何使用
Axes
对象。

3.1. 基本坐标系

使用
Axes
对象创建一个基本的二维坐标系,设置
x轴

y轴
的范围,并显示坐标轴上的
数字

刻度线

下面的示例设置
x轴
范围
[-5,5]

y轴
范围
[-3,3]
,坐标轴上
以1为间隔
显示数字和刻度线。

# x轴范围从-5到5,间隔为1
# y轴范围从-3到3,间隔为1
# 显示坐标轴上的数字
Axes(
    x_range=[-5, 5, 1],
    y_range=[-3, 3, 1],
    axis_config={"include_numbers": True},
)

3.2. 自定义坐标轴样式

自定义坐标轴的样式,包括轴的颜色、线条宽度、刻度线的长度和是否显示箭头等。

下面的示例将
x轴
设置为红色,线条宽度为2;
y轴
设置为蓝色,线条宽度为1.5,且
x轴

y轴
末端均显示不同的箭头。

axes = Axes(
    x_range=[-5, 5, 1],
    y_range=[-3, 3, 1],
    x_axis_config={
        "color": RED,
        "stroke_width": 5,
        "include_tip": True,  # 显示箭头
        "tip_shape": ArrowSquareTip,
    },
    y_axis_config={
        "color": BLUE,
        "stroke_width": 1.5,
        "include_tip": True,
        "tip_shape": StealthTip,
    },
    axis_config={
        # 加长特定数字的刻度线
        "numbers_with_elongated_ticks": [0, -3, 3],
        "font_size": 20,
    },  
)

3.3. 绘制函数曲线

坐标系上绘制一个函数曲线。

比如,下面的示例绘制一个抛物线($ y=x^2
\()和一个三角函数曲线(\)
y=\sin(\theta) $)。

axes = Axes(
    x_range=[-5, 5, 1],
    y_range=[-5, 5, 1],
    x_axis_config={
        "color": RED,
    },
    y_axis_config={
        "color": BLUE,
    },
    axis_config={
        "include_numbers": True,
        "font_size": 20,
        "include_tip": True,
        "tip_shape": StealthTip,
        "tip_length": 2,
    },
)

# 抛物线
func = lambda x: x**2
graph = axes.plot(func, x_range=[-2, 2], color=GREEN)
self.play(Create(graph), run_time=run_time)

# 三角函数
func = lambda x: np.sin(x)
graph = axes.plot(func, x_range=[-5, 5], color=YELLOW)
self.play(Create(graph), run_time=run_time)

3.4. 添加标签和注释

在坐标轴上添加标签,并在函数曲线上添加注释,以解释图形的含义。

下面的示例基于上一节的函数曲线,标出坐标轴名称
x

y
,并且选择抛物线上的一个点,标出点的坐标。

axes = Axes(
    x_range=[-5, 5, 1],
    y_range=[-5, 5, 1],
    x_axis_config={
        "color": RED,
    },
    y_axis_config={
        "color": BLUE,
    },
    axis_config={
        "include_numbers": True,
        "font_size": 20,
        "include_tip": True,
        "tip_shape": StealthTip,
        "tip_length": 2,
    },
)

# 绘制曲线
func = lambda x: x**2
graph = axes.plot(func, x_range=[-2, 2], color=GREEN, stroke_width=2)

# 添加标签
x_label = axes.get_x_axis_label(
    MathTex("x", font_size=25, color=RED),
    direction=UP,
)
y_label = axes.get_y_axis_label(
    MathTex("y", font_size=25, color=BLUE),
    direction=RIGHT,
)

# 标记一个点
x = 1.5
y = x * x
d = Dot(axes.coords_to_point(x, y), color=YELLOW)
txt = Matrix([[x], [y]]).scale(0.5).next_to(d.get_center(), RIGHT)

4. 附件

文中的代码只是关键部分的截取,完整的代码共享在网盘中(
axes.py
),

下载地址:
完整代码
(访问密码: 6872)

大家好,我是汤师爷~

让我们深入探讨一下多租户系统的概念模型。概念模型是多租户系统的"骨架",帮助我们理解系统各部分的组织和运作方式。

多租户的核心概念模型

  • 租户
    :通常指一个企业客户,不同租户之间的数据和行为是相互隔离的。
  • 用户
    :某个租户内的具体使用者,使用账号、密码等信息登录到 SaaS 系统,使用软件服务。
  • 组织
    :如果租户是企业客户,通常会有自己的组织架构。
  • 员工
    :指组织内部的具体成员。
  • 解决方案
    :为解决客户的特定业务问题,SaaS 服务商将产品与服务组合打包,提供整体解决方案。
  • 产品
    :SaaS 服务商售卖给客户的软件应用,能够帮助客户实现端到端流程的闭环解决方案。
  • 资源域
    :用于运行一个或多个软件应用的一套云资源环境。
  • 云资源
    :SaaS 产品一般部署在各类云平台上(如阿里云、腾讯云、华为云等),这些平台提供的计算、存储、网络、容器等资源,被抽象为云资源。

SaaS平台可以创建和管理多个平台用户、租户和资源域,这样的模型结构,可以让多个企业客户和用户能在同一平台上运行,而彼此之间的数据和资源独立。

一个平台用户可以关联到多个租户。例如,张三作为一个平台用户,可以同时属于租户A和租户B。这种设计让平台用户能够灵活切换所属租户,方便在不同企业账号间工作。

在单个租户中,也可以拥有多个用户,让企业内多名员工共享租户的资源和服务。

单个租户可以订购多个解决方案,一个解决方案往往包含多个产品,以满足企业客户的多样需求。这些产品可以运行在特定的资源域,保证不同产品在同一租户中的高效管理和资源隔离。

租户内的组织架构支持上下级关系,在单个组织单元内,可以配置多个员工,并将员工与平台用户绑定,便于员工通过自己的账户访问相关服务。

概念模型的应用场景示例

1、租户与内部模型关系

在SaaS产品中,租户是最顶层的概念,可以理解为一个大房子的租赁人,而房子内部的组织、用户、产品、云资源等模型就像是这个房子里的各种家具和设施。

换句话说,租户是SaaS产品为每个客户或企业专门划分出的独立空间,而组织、用户、产品、云资源等模型则是租户内部的细分结构,为不同的使用需求和权限分配提供支持。

  • 租户:相当于整套房子的所有权,即租户拥有这套房子在SaaS平台中的使用权,是所有内部资源的顶层管理者。
  • 组织:类似房间的布局,每个房间有特定的功能(比如子公司、部门),组织架构帮助企业在平台内映射现实中的管理层级关系。
  • 用户:就像在房子里活动的人员,用户被赋予不同的角色和权限,决定了谁可以进入哪些房间,谁可以使用哪些家具。
  • 产品:如同家里各种各样的家具、电器、设施,满足不同的需求。不同租户可以选择不同的产品组合,随需增加或减少,来满足他们的业务需求。
  • 云资源:类似于水电煤等基础设施,支持房子里的各项功能正常运行,确保产品稳定、流畅地提供服务。

通过这种类比可以看出,SaaS产品将租户作为最顶层的概念,为企业提供了一套独立空间,租户内部的各项资源则在这个框架下被灵活管理和使用,让企业客户可以获得定制化的服务和资源隔离的安全保障。

2、租户身份识别

在多租户SaaS系统中,无论采用哪种隔离模式,准确识别租户身份并获取相应资源配置是非常关键的。

当用户登录SaaS系统后,系统会生成并返回租户上下文信息。这个上下文信息包括用户关联的租户和对应的隔离模式(如共享或独立资源)。

租户上下文信息会附加在每次系统交互中,贯穿整个系统调用链。从用户请求到系统内部处理的每一步,都保留了租户上下文。这样,上游的请求处理模块就可以知道如何路由和访问下游资源。

租户上下文信息让系统能够在请求传递过程中精准识别租户身份和配置。系统会根据上下文动态选择数据库、应用实例或网络资源,实现数据和资源隔离,确保不同租户的访问互不干扰。

3、租户计费计量管理

租户的计费计量管理,是SaaS平台不可或缺的一部分。不同的隔离模式下,计费方式有所不同。

竖井隔离模式下,计费相对简单。因为每个租户使用的资源(计算、存储、网络等)都是单独分配的,就像每个租户有自己的一块“独立地盘”。

因此,我们只需统计每个租户占用的资源量,就能计算出费用。这种模式逻辑清晰,一目了然。

共享模式下,计费则比较复杂。因为多个租户共享同一资源池,理想情况是,确保每个租户只为自己实际用到的部分买单。

通常,我们会综合考量几个指标,比如请求的数量、存储容量、创建的数据对象数量等。通过这些数据的组合,我们可以相对准确算出每个租户的费用。

本文已收录于,我的技术网站:
tangshiye.cn
里面有,算法Leetcode详解,面试八股文、BAT面试真题、简历模版、架构设计,等经验分享。