2024年9月

前言

大家好,推荐一个.NET 8.0 为核心,结合前端 Vue 框架,实现了前后端完全分离的设计理念。它不仅提供了强大的基础功能支持,如权限管理、代码生成器等,还通过采用主流技术和最佳实践,显著降低了开发难度,加快了项目交付速度。

如果你需要一个高效的开发解决方案,本框架能帮助大家轻松应对挑战,实现业务目标。

项目介绍

YuebonCore 是一款基于 .NET 8.0 的现代化权限管理及快速开发框架,专为前后端分离设计。它集成了组织机构管理、角色用户管理、权限分配、多系统和多应用支持、定时任务调度、业务单据编码规则以及代码生成器等核心模块,为开发者提供了一站式的开发体验。

该框架充分利用了最新的技术栈,包括 ASP.NET Core MVC、SqlSugar ORM、WebAPI、Swagger 文档自动生成工具以及 Vue.js 前端框架,确保了开发过程中的高效与便捷。

YuebonCore 架构简洁、易用性强,非常适合中小规模项目的快速启动与迭代。

功能特点

  • 开源许可:
    采用 MIT 许可协议,完全开源。
  • 主流框架:
    基于流行技术栈,易于学习和使用。

  • 二次开发:
    支持灵活的定制化开发,满足大多数项目需求。

  • 代码生成器:
    减少 70% 的重复编码工作,提高开发效率。

  • 精细权限控制:
    导航菜单与功能按钮级权限管理。

  • 数据权限管理:
    精确控制数据访问范围,如部门、组或公司级别。

  • 常用类封装:
    日志、缓存、验证等功能模块。

适用范围

适用范围广泛,可用于开发包括 OA、ERP、BPM、CRM、WMS、TMS、MIS、BI、电商平台后台、物流管理系统、快递管理系统以及教务管理系统在内的各类管理软件。

内置功能

1、系统设置:对系统动态配置常用参数。

2、用户管理:用户是系统操作者,该功能主要完成系统用户配置。

3、组织机构:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。

4、角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。

5、字典管理:对系统中经常使用的一些较为固定的数据进行维护。

6、功能模块:配置系统菜单,操作权限,按钮权限标识等。

7、定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。

8、代码生成:前后端代码的生成(.cs、.vue、.js)代码。

9、日志管理:系统正常操作日志、登录日志记录和查询;系统异常信息日志记录和查询。

10、多应用管理:支持应用调用 api 授权控制。

11、多系统管理:实现各子系统的统一管理和授权。

13、业务单据编码规则:可以按常量、日期、计数、时间等自定义业务单据编码规则。

14、短信和邮件:集成腾讯云短信通知和 EMail 发送通知

15、支持租户模式

16、支持用户定义主题风格

17、支持一主多从数据库读写分离

项目技术

前端目前采用 Vue 独立前端和 ASP.NET Core MVC 模式,使用的技术栈有些区别,后期将侧重于 Vue 端的优化运维。

1、前端技术

基于 Vue3、Vite、Vuex、Vue-router 、Vue-cli 、axios 和 element-plus

前端采用 VS Code 工具开发

2、传送门

element-plus 官网

https://element-plus.org/#/zh-CN

vue-element-admin

https://panjiachen.gitee.io/vue-element-admin-site/zh/guide/

3、后端技术

  • 核心框架:.NET 8.0 +SqlSugar + AutoMapper + WebAPI + swagger
  • 定时计划任务:Quartz.Net 组件
  • 安全支持:过滤器、Sql 注入、请求伪造
  • 服务端验证:实体模型验证、自己封装 Validator
  • 缓存框架:微软自带 Cache、Redis
  • 日志管理:Log4net、登录日志、操作日志
  • 工具类:NPOI、MiniProfiler 性能分析、验证码、丰富公共功能

项目结构

Yuebon.NetCore 解决方案包含

  • Yuebon.Commons[基础类库]
    :包框架的核心组件,包含一系列快速开发中经常用到的 Utility 辅助工具功能,框架各个组件的核心接口定义,部分核心功能的实现;

  • Yuebon.Security.Core[权限管理类库]
    :以 Security 为基础实现以角色-功能、用户-功能的功能权限实现,以角色-数据,用户-数据的数据权限的封装

  • Yuebon.AspNetCore[AspNetCore 类库]
    ,提供 AspNetCore 的服务端功能的封装,支持 webapi 和 webmvc 模式,同时支持插件式开发;

  • Yuebon.WebApp[管理后台]:基于 aspnet core mvc 实现了权限管理和 CMS 部分管理后台。

  • Yuebon.Cms.Core[CMS 基础类库],包含文章管理、广告管理等内容,以此做案例给大家开发参考

  • Yuebon.WebApi[webapi 接口]
    :为 Vue 版或其他三方系统提供接口服务。

DataBase 是最新数据库备份文件,目前支持 MS SQL Server 和 MySql。

项目效果

1、管理平台体验地址:http://default.ts.yuebon.com 有租户管理模块

2、测试租户体验地址:http://tenant1.ts.yuebon.com 无租户管理模块

体验账号密码均为:admin/admin888

登录页面

系统模块和功能管理

角色管理

代码生成器
支持一键生成服务端代码和前端代码,复制粘贴简单快速高效实现功能

项目地址

Gitee:
https://gitee.com/yuebon/YuebonNetCore

WebAPI:
http://netcoreapi.ts.yuebon.com

官方文档:
http://docs.v.yuebon.com/

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号
[DotNet技术匠]
社区,与其他热爱技术的同行一起交流心得,共同成长!

六,Spring Boot 容器中 Lombok 插件的详细使用,简化配置,提高开发效率

@


1. Lombok 介绍

Lombok 作用:

  1. 简化 Java Bean开发,可以使用 Lombok 的注解让代码更加简洁。
  2. Java项目中,很多没有技术含量但又必须存在的代码;比如:Pojo 的 getter/setter/toString ;异常处理:I/O流的关闭操作等等。
  3. Java项目中,很多没有技术含量但又必须存在的代码:比如:这些代码既没有技术含量,又影响着代码的美观,Lombok 应运而生。

Spring Boot 和 IDEA 官方支持

  1. IDEA 2020已经内置了 Lombok插件
  2. Spring Boot 2.1.x之后的版本也在 Stater中内置了 Lombok 依赖

2. Lombok 常用注解

@Data:注解在类上,提供类所有属性的getting和setting方法,此外还提供了equals,canEqual,hashCode,toString方法
@Setter: 注解在属性上,为属性提供 setting 方法,注解在类上,则为类中的所有属性提供 set()方法。
@Getter: 注解在属性上,为属性提供 getting 方法注解在类上,则为类中的所有属性提供 get()方法。
@Log4j:注解在类上:为类提供了一个属性名为: log 的 log4j的日志对象
@NoArgsConstructor: 注解在类上,为类提供一个无参的构造方法,(一定会提供)
@AllArgsConstructor: 注解在类上,为类提供一个全参的构造方法
@Cleanup: 可以关闭流
@Builder: 被注解的类加个构造者模式
@Synchronized: 加同步锁 
@SneakyThrows: 等同于try/catcher捕获异常
@NonNull:如果给参数加个这个注解,参数为null会抛出空指针异常
@Value: 注解和@Value类似,区别在于它会把所有成员变量默认定义为 private final 修饰,并且不会生产set()方法 

下面我们测试,使用几个,在开发中比较常用的
注解

首先想要使用上 Lombok 需要导入相关
jar
依赖,也可以不用特别导入,直接使用 Spring Boot 自带内置的即可。

在这里插入图片描述

   <!--        引入 lombok ,使用版本仲裁-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.rainbowsea</groupId>
    <artifactId>springboot_lombok</artifactId>
    <version>1.0-SNAPSHOT</version>
    <!--    导入SpringBoot 父工程-规定写法-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.3</version>
    </parent>


    <dependencies>
        <!--    导入web项目场景启动器:会自动导入和web开发相关的jar包所有依赖【库/jar】-->
        <!--    后面还会在说明spring-boot-starter-web 到底引入哪些相关依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--        引入 lombok ,使用版本仲裁-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

</project>

2.1 @ToString

@ToString: // 在编译时,生成 toString,注意:默认情况下,会生成一个无参构造器。

在这里插入图片描述

package com.rainbowsea.springboot.bean;


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;




@ToString  // 在编译时,生成 toString,注意:默认情况下,会生成一个无参构造器
public class Furn {
    private Integer id = 100;
    private String name = "张三";
    private Double price = 999.0;


}

使用 lombok 注解简化代码,可以通过 idea 自带的反编译功能 target,看Furn.class的源码,就
可以看到生成的完整代码。

如果目录当中没有 target 目录显示,可以进行如下操作:

在这里插入图片描述

在这里插入图片描述

2.2 @Setter

@Setter 注解: 注解在属性上,为属性提供 setting 方法,注解在类上,则为类中的所有属性提供 set()方法。

首先,我们先注解使用在属性上。

在这里插入图片描述

package com.rainbowsea.springboot.bean;


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;


public class Furn {

    @Setter // 注解在属性上,编译生成一个,该属性的 set()方法:默认情况下,会生成一个无参构造器
    private Integer id = 100;

    private String name = "张三";
    private Double price = 999.0;


}

在这里插入图片描述

添加到类上,为类中所有的属性都添加,set() 方法。

在这里插入图片描述

在这里插入图片描述

2.3 @Data

@Data :注解在类上,提供类所有属性的getting和setting方法,此外还提供 equals,canEqual,hashCode,toString方法, @RequiredArgsConstructor

特别说明: @Data 中的 @RequiredArgsConstructor
在我们写controller 或是 Service层的时候,需要注入很多的 mapper接口或者另外的service接口,这时候就会写很多
的@Autowired注解,代码看起来很乱。Lombok 提供了一个注解:
@RequiredArgsConstructor(onConstructor=@_(@AutoWired))
写在类上可以代替@Autowired注解,需要注意的时在注入时,需要用final定义,或者使用 @notnull注解

在这里插入图片描述

package com.rainbowsea.springboot.bean;


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;



@Data// 注解等价使用了,如下注解: @Getter,@Setter,@RequiredArgsConstructor @ToString,@EqualsAndHas
public class Furn {

    private Integer id = 100;

    private String name = "张三";
    private Double price = 999.0;


}

在这里插入图片描述

2.4 @AllArgsConstructor

@AllArgsConstructor:在编译时,会生成全参数构造器

在这里插入图片描述

package com.rainbowsea.springboot.bean;


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;



@AllArgsConstructor // 在编译时,会生成全参数构造器
public class Furn {

    private Integer id = 100;

    private String name = "张三";
    private Double price = 999.0;


}

在这里插入图片描述

注意:这里:我们会发现一点,就是有全参数构造器,但是,却没有生成一个默认的“无参数构造器”

2.5 @NoArgsConstructor

@NoArgsConstructor: 在编译时,会生成无参构造器(一定会生成),不会受到其它的

在这里插入图片描述

package com.rainbowsea.springboot.bean;


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;


@NoArgsConstructor //在编译时,会生成无参构造器(一定会生成),不会受到其它的
public class Furn {

    private Integer id = 100;

    private String name = "张三";
    private Double price = 999.0;


}

在这里插入图片描述

特别说明:

特别说明,虽然上面的@Data,@ToString注解等等,默认情况下都会生成一个无参构造器,但是当你使用了多个注解的时候,可能会覆盖掉无参构造器。 但是当我们有其它多个构造器生成时,你如果你希望仍然有无参构造器就需要使用 @NoArgsConstructor 注解了,因为 @NoArgsConstructor是一定会生成一个无参构造器的(无参构造器很重要,因为框架的使用是涉及到反射机制的,而反射机制,需要一个无参构造器,否则你就无法进行反射获取 bean对象,框架也就无法使用了)。

如下:测试。我们添加上 @Data注解和 @AllArgsConstructor 注解。可以明显的发现,
默认的无参数构造器被覆盖掉了

在这里插入图片描述

所以我们需要添加上:@NoArgsConstructor注解,因为@NoArgsConstructor注解,在编译时,会生成无参构造器(一定会生成),不会受到其它的影响,被覆盖。如图

在这里插入图片描述

3. 在 idea 中 安装 lombok插件

不装插件也可以用基本的注解比如:@Data,@Getter...
但是不能使用其扩展功能,比如日志输出...,所以我们还是安装一下,也比较简单。
直接去插件商城,搜索: Lombok 即可

在这里插入图片描述

在这里插入图片描述

package com.rainbowsea.springboot.controller;


import com.rainbowsea.springboot.bean.Furn;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@Slf4j
@RestController  // @Controller + @ResponseBody
public class HiController {


    @Autowired
    private Furn furn;


    @RequestMapping("/hi") // 设置请求映射路径
    public String hi(){
        log.info("furn-"+furn);

        // 占位符方式输出
        log.info("furn={} myfurn={}",furn,furn);


        return  "hi word";
        // 使用 slf4j日志输出
    }
}

运行测试:

在这里插入图片描述

4. 总结:

  1. 熟悉常用的 Lombok 注解,提供开发效率。
  2. 基本上@注解都会生成一个默认的无参构造器,但是当存在多个注解的时候,这个默认的无参构造器,可能会被覆盖掉,从而导致无法不会被生成。但是,框架的上的使用,基本上都是需要使用上
    反射
    机制的,而反射机制是
    必须要有一个无参构造器
    才可以进行反射,获取到对应的 Bean对象。给框架使用。所以,一般的 bean 对象都会添加上一个@NoArgsConstructor注解该在编译时,会生成无参构造器(一定会生成),不会受到其它的

6. 最后:

“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”

在这里插入图片描述

带箭头的直线
就是有方向的直线,既可以用来表示
矢量
,也可以用来
标记
某个关键位置。
manim
中提供了
4种
常用的
带箭头的直线
模块:

  1. Arrow
    :单箭头的直线
  2. DoubleArrow
    :双箭头的直线
  3. LabeledArrow
    :带标签的直线
  4. Vector
    :向量

其中,
DoubleArrow

LabeledArrow

Vector
都继承自
Arrow
模块,

Arrow
模块继承自上一次介绍的
Line
模块。

Vector
表面看起来
Arrow
一样,也是一个单箭头直线,它们的主要区别在于:
Arrow
需要设置
起点

终点
,而
Vector
只要设置
终点
,它的的
起点
固定为
坐标系原点

image.png
带箭头直线系列
的模块位于
manim
中的
Mobject
之下。

1. 主要参数

Arrow
模块的主要参数有:

参数名称 类型 说明
start Point3D 起点
end Point3D 终点
stroke_width float 箭头的粗细
buff float 箭头的长短
max_tip_length_to_length_ratio float 箭头大小与线的粗细的比例,此值越大,箭头相对于线越大
max_stroke_width_to_length_ratio float 线的粗细与箭头大小的比例,此值越大,线相对于箭头线越粗
tip_shape VMobject 箭头的类型,本质就是一个几何对象

其中,
start

end
参数继承自
Line
模块。

DoubleArrow
模块的参数与
Arrow
基本一致,只是它两头都有箭头。

Vector
模块的主要参数有:

参数名称 类型 说明
direction Point2D/Point3D 向量的方向,相当于终点

Vector
模块只需要设置终点,也就是
direction
。其他参数可以参考
Arrow
模块。

模块
LabeledArrow
可以在箭头直线上加一个便签,用于添加额外的说明信息。

参数名称 类型 说明
label str 标签中文字,可支持数学公式
label_position float 标签位置,默认在直线正中间
font_size float 标签字体大小
label_color Color 标签颜色
label_frame bool 是否显示标签背景
frame_fill_color Color 标签背景颜色
frame_fill_opacity float 标签背景透明度

2. 主要方法

Arrow
模块的主要方法有:

名称 说明
get_default_tip_length 获取箭头的长度
get_normal_vector 获取向量的法线
reset_normal_vector 重置向量的法线
scale 缩放向量

DoubleArrow

LabeledArrow
的方法和
Arrow
一样。

Vector
除了上面
Arrow
的方法,还有一个自己特有的方法:

名称 说明
coordinate_label 基于向量的坐标显示向量的值

比如:

class ArrowExample(Scene):
    def construct(self):
        vec_1 = Vector([1, 3])
        vec_2 = Vector([-2, -2])
        label_1 = vec_1.coordinate_label(color=BLUE)
        label_2 = vec_2.coordinate_label(color=YELLOW)

        self.add(vec_1, vec_2, label_1, label_2)

vector.png

3. 使用示例

3.1. 箭头和线的比例

通过
buff

max_stroke_width_to_length_ratio

max_tip_length_to_length_ratio
参数,
可以调整箭头和直线的比例,使之符合不同动画场景的需要。

# buff
vg = VGroup()
for buff in np.arange(0, 2, 0.5):
    vg.add(
        Arrow(
            buff=buff,
            start=2 * LEFT,
            end=2 * RIGHT,
        )
    )

vg.arrange(DOWN)
vg.move_to(2.5 * LEFT)
self.play(Create(vg), run_time=run_time)

# max_stroke_width_to_length_ratio
vg = VGroup()
for i in np.arange(0, 5, 1):
    vg.add(
        Arrow(
            max_stroke_width_to_length_ratio=i,
        ),
    )

vg.arrange(DOWN)
self.play(Create(vg), run_time=run_time)

# max_tip_length_to_length_ratio
vg = VGroup()
for i in np.arange(0, 0.3, 0.06):
    vg.add(
        Arrow(
            max_tip_length_to_length_ratio=i,
        ),
    )

out.gif

3.2. 箭头的样式

箭头的样式不仅仅只有三角形,
manim
中内置了多种不同的箭头样式。

Arrow(
    start=2 * LEFT,
    end=2 * RIGHT,
    tip_shape=ArrowCircleFilledTip,
)
Arrow(
    start=2 * LEFT,
    end=2 * RIGHT,
    tip_shape=ArrowCircleTip,
)
Arrow(
    start=2 * LEFT,
    end=2 * RIGHT,
    tip_shape=ArrowSquareFilledTip,
)
Arrow(
    start=2 * LEFT,
    end=2 * RIGHT,
    tip_shape=ArrowSquareTip,
)
Arrow(
    start=2 * LEFT,
    end=2 * RIGHT,
    tip_shape=ArrowTriangleFilledTip,
)
Arrow(
    start=2 * LEFT,
    end=2 * RIGHT,
    tip_shape=ArrowTriangleTip,
)
Arrow(
    start=2 * LEFT,
    end=2 * RIGHT,
    tip_shape=StealthTip,
)

out.gif

3.3. 向量的样式

向量
是一种特殊的
带箭头直线
,它的起点默认在坐标原点。
其他的属性也可以和上面带箭头的一样进行设置。

vec1 = Vector([1, 1], color=BLUE)

vec2 = Vector(
    [-2, 1],
    color=RED,
    tip_shape=ArrowSquareTip,
)
label2 = vec2.coordinate_label(color=RED)

vec3 = Vector([2, -1.5], color=GREEN)

out.gif

3.4. 带标签的箭头

最后,
带箭头
的直线和普通的直线一样,也可以添加标签信息,用于对直线进行说明。

LabeledArrow(
    label="y=kx+b",
    font_size=25,
    start=start,
    end=end,
)
LabeledArrow(
    label=txt1,
    start=start,
    end=end,
)
LabeledArrow(
    label="z=\sqrt{x^2+y^2}",
    font_size=25,
    start=start,
    end=end,
    label_color=RED,
    label_frame=False,
)
LabeledArrow(
    label=txt2,
    start=start,
    end=end,
    frame_fill_color=GREEN,
    frame_fill_opacity=0.8,
)

out.gif

4. 附件

文中完整的代码放在网盘中了(
arrow.py
),
下载地址:
完整代码
(访问密码: 6872)

人生有那么一首诗,往往当你拥有他的时候,你没有读懂他,可是当你读懂他的时候,你却失去了他,这首诗就是青春。

“一寸光阴一寸金,寸金难买寸光阴”,学生时代的作文中,已经被我们用烂了的词汇,时至今日,终于才深刻理解这句话的重要意义。光阴的确是无价的,一旦错过却无法追回,一寸光阴又何止一寸金呢。古人说过“三十而立,四十不惑,五十知天命”,几千年得出的经验,还真当是诚不我欺呀。

二十多岁刚出学校,忙着立足于工作,心高气傲。那时一心想学习更多的技能,更好的工作,获得更高的薪水,看到更多的风景。直到十多年之后的三十岁才逐渐认识到,世界上的工作千万种,然而没有一种是可以让人富有的,我们从小被灌输的那句勤劳致富,实际上是一句谎话。那些美好的童话故事,在实际生活中根本就不会发生。

三十岁之后的我们大多数按部就班成家立业,家庭的琐事会消耗一部分精力,当然也同样会带来慰藉。

三十五岁之后我们终于认识到,身体是革命的本钱,原来不是一句空话。

在即将到来的四十岁人生当口,确如古人所说,到了不惑的年纪。其实不惑是一直都存在的,只不过到了这样的年龄,才逐渐开始思考起来。

刚出学校,我们心气很高,海阔凭鱼跃,天高任鸟飞,每个人对未来都有美好的期许。然而经过这么多年社会的磨砺与摔打,逐渐磨平了棱角,可以说是某种程度上的认命,或者是看清了现实。其实我不认可这种说法,认命这个词语在我看来有消极的成份,我觉得另外一个词语或许更好,那便是成熟。世界上只有一种英雄主义,那就是看清生活的真相之后,依然热爱生活。我希望每个人都做自己生活的英雄。

其实我一直对年龄并没有特别敏感的感觉,一直觉得来日方长。就像单位的同事从九零后变成九五后,再到后来的零零后,我并没有意识到年龄会有什么问题。

直到一个周末的午后,我梦到了小时候的老家,那是一个夏天,老家后面的河里涨大水,淹没了河堤的一角,我与几个高中的同伴跳过被河水淹没的地方,河堤对岸的小卖部盖了新楼,我们感到惊奇。我站在河堤上,端详新修的小卖部,看到它背后黄昏的晚霞,像是一副巨大的绚烂水彩画,好美,我急忙拿出手机拍照。然后我们跳过淹水的地方,回了岸边,看到有几个人坐在岸边钓鱼。稻田夹着宽阔的土路,与远方的天青色交汇在一起。突然一阵寒意袭来,我惊醒,发现只是一场梦。赶紧翻看手机相册,却并没有找到那张晚霞,怅然若有所失。我突然意识到,逝去的时间永远也追不回来了。

其实关于年龄的增长是早就有预示的,只不过常常被我们忽略罢了。不知从什么时候开始,我们会被生活中的细微处所感动,喜欢每一次的久别重逢,喜欢彼此带着笑意的“好久不见”,喜欢带着一身风霜却温暖如初的拥抱。这大概也是年龄增长的暗示吧。

这些年极少看娱乐节目的我,会追《向往的生活》,我喜欢那种老朋友一起吃饭闲聊的生活氛围。那时候常常想拥有一方小的院落,在略显燥热的傍晚,约上三五知己好友,于繁星簇拥的月光下,畅聊过往,让欢歌笑语恣意挥洒,弥漫在酒香与烟雾里。这样的人间烟火气大概是最平凡的幸福吧。所以你看看,二十岁的我大概是不会想这些的。

有人说生活像是围城,外面的人像想进来,里面的人想出去。就好像萨冈说过:“所以漂泊的人都渴望平静、童年和杜鹃花,正如所以平静的人都幻想乐队、伏特加和醉生梦死”。这也许是为什么生活于城市钢筋混凝土中,有时候却向往乡村的荒野小径。而生活在乡村的烟火里,却向往城市的灯红酒绿。

我曾经对这个问题有过思考,这并非是我们少无适俗韵,性本爱丘山。只是在疲惫的城市中也需要一些宁静的生活作为调剂。归根结底还是大多数人,无法做到独善其身罢了。人是社会属性的动物,所以难免会被周围所影响,小隐隐于野,中隐隐于市,大隐隐于朝,对于大多数人来说,是无法在城市生活中,保持一颗隐的心境的。

这两天我第一次注意到到办公楼下的草坪由绿色变成了黄色,这让我想起湖北的秋冬应该也快要来临了吧,在广东已经很久没有家乡秋冬的感觉了。

我有一种独特的放松方式,蹲在某一个角落,观察窗外世界的变化,比如从炙热的中午到闲暇的下午,从黄昏到日落,看外面的人来车往,好像是在看别人的生活,一切井然有序,却与我无关,而我,只是一个旁观者。

我们是俗人,无法做到像颜斶那样返璞归真,晚食以当肉,安步以当车,无罪以当贵,清静贞正以自虞。当我有一次读到史铁生,他说:“我四肢健全时,常抱怨周围环境糟糕;瘫痪后,怀念当初可以行走奔跑的日子;几年后长了褥疮,又怀念起前两年安稳坐在轮椅上的时光;后来得了尿毒症,又开始怀念当初长褥疮的时候。人生无所谓幸与不幸,只是两种不同境遇的比较罢了”。我突然有一点开窍似的恍然大悟,其实我们最应该珍惜的恰好是眼前的时光,经过那些焦虑与不安的日子,才明白,与生活和解,活在当下,却是如此弥足珍贵。

四十岁,是一个人生节点,蹉跎半生,有消极也有积极,有过笑也有过泪,与以往的日子相比,也许相同,也许不同。相同的是我们仍然有一颗不甘的心,不同的是我们逐渐学会了处之泰然,活在当下。如果还要再说点什么,当十年之后,已经五十岁的我们再来回首过往,一定会想,如果时间能够倒流,回到十年前的四十岁那该是多么美好啊,然而我们在当下的四十岁此时此刻却浑然不知,一如十年前三十岁的我们一样,当时只道是寻常罢了。

自言自语,无病呻吟,以一篇短文来作为结尾吧:

最怕问初衷,幻梦成空。
年少立志三千里,踌躇百步无寸功。
懵懂半生,庸碌尘世中。
转眼高堂皆白发,儿女蹒跚学堂中
碎银几两催人老,心仍少,皱纹上眉中。
浮生醉酒回梦里,青葱人依旧
只叹当时,太匆匆。

Less-Attention Vision Transformer
利用了在多头自注意力(
MHSA
)块中计算的依赖关系,通过重复使用先前
MSA
块的注意力来绕过注意力计算,还额外增加了一个简单的保持对角性的损失函数,旨在促进注意力矩阵在表示标记之间关系方面的预期行为。该架构你能有效地捕捉了跨标记的关联,超越了基线的性能,同时在参数数量和每秒浮点运算操作(
FLOPs
)方面保持了计算效率。

来源:晓飞的算法工程笔记 公众号

论文: You Only Need Less Attention at Each Stage in Vision Transformers

Introduction


近年来,计算机视觉经历了快速的增长和发展,主要得益于深度学习的进步以及大规模数据集的可获得性。在杰出的深度学习技术中,卷积神经网络(
Convolutional Neural Networks
,
CNNs
)被证明特别有效,在包括图像分类、目标检测和语义分割等广泛应用中展现了卓越的性能。

受到
Transformer
在自然语言处理领域巨大成功的启发,
ViT

Vision Transformers
)将每幅图像划分为一组标记。这些标记随后被编码以生成一个注意力矩阵,作为自注意力机制的基础组成部分。自注意力机制的计算复杂度随着标记数量的增加而呈平方增长,且随着图像分辨率的提高,计算负担变得更加沉重。一些研究人员尝试通过动态选择或标记修剪来减少标记冗余,以减轻注意力计算的计算负担。这些方法在性能上已证明与标准
ViT
相当。然而,涉及标记减少和修剪的方法需要对标记选择模块进行细致设计,可能导致关键标记的意外丢失。在本研究中,作者探索了不同的方向,并重新思考自注意力的机制。发现在注意力饱和问题中,随着
ViTs
层数的逐渐加深,注意力矩阵往往保持大部分不变,重复前面层中观察到的权重分配。考虑到这些因素,作者提出以下问题:

在网络的每个阶段,从开始到结束,是否真的有必要始终一致地应用自注意力机制?

在本文中,作者提出通过引入少注意力
ViT

Less-Attention Vision Transformer
)来修改标准
ViT
的基本架构。框架由原始注意力(
Vanilla Attention
,
VA
)层和少注意力(
Less Attention
,
LA
)层组成,以捕捉长范围的关系。在每个阶段,专门计算传统的自注意力,并将注意力分数存储在几个初始的原始注意力(
VA
)层中。在后续的层中,通过利用先前计算的注意力矩阵高效地生成注意力分数,从而减轻与自注意力机制相关的平方计算开销。此外,在跨阶段的降采样过程中,在注意力层内集成了残差连接,允许保留在早期阶段学习到的重要语义信息,同时通过替代路径传输全局上下文信息。最后,作者仔细设计了一种新颖的损失函数,从而在变换过程中保持注意力矩阵的对角性。这些关键组件使作者提出的
ViT
模型能够减少计算复杂性和注意力饱和,从而实现显著的性能提升,同时降低每秒浮点运算次数(
FLOPs
)和显著的吞吐量。

为验证作者提出的方法的有效性,在各种基准数据集上进行了全面的实验,将模型的性能与现有最先进的
ViT
变种(以及最近的高效
ViT
)进行了比较。实验结果表明,作者的方法在解决注意力饱和并在视觉识别任务中取得优越性能方面非常有效。

论文的主要贡献总结如下:

  1. 提出了一种新颖的
    ViT
    架构,通过重新参数化前面层计算的注意力矩阵生成注意力分数,这种方法同时解决了注意力饱和和相关的计算负担。

  2. 此外,提出了一种新颖的损失函数,旨在在注意力重新参数化的过程中保持注意力矩阵的对角性。作者认为这一点对维护注意力的语义完整性至关重要,确保注意力矩阵准确反映输入标记之间的相对重要性。

  3. 论文的架构在包括分类、检测和分割在内的多个视觉任务中,始终表现优异,同时在计算复杂度和内存消耗方面具有类似甚至更低的特点,胜过几种最先进的
    ViTs

Methodology


Vision Transformer


\(\mathbf{x} \in \mathbb{R}^{H \times W \times C}\)
表示一个输入图像,其中
\(H \times W\)
表示空间分辨率,
\(C\)
表示通道数。首先通过将图像划分为 $N = \frac{HW}{p^{2}} $ 个块来对图像进行分块,其中每个块
\(P_i \in \mathbb{R}^{p \times p \times C}\left(i \in \{1, \ldots, N\} \right)\)
的大小为
\(p \times p\)
像素和
\(C\)
通道。块大小
\(p\)
是一个超参数,用于确定标记的粒度。块嵌入可以通过使用步幅和卷积核大小均等于块大小的卷积操作提取。然后,每个块通过不重叠的卷积投影到嵌入空间
\(\boldsymbol{Z} \in \mathbb{R}^{N\times{D}}\)
,其中
\(D\)
表示每个块的维度。

  • Multi-Head Self-Attention

首先提供一个关于处理块嵌入的经典自注意力机制的简要概述,该机制在多头自注意力块(
MHSAs
)的框架内工作。在第
\(l\)

MHSA
块中,输入
\(\boldsymbol{Z}_{l-1}, l \in \{1,\cdots, L\}\)
被投影为三个可学习的嵌入
\(\{\mathbf{Q,K,V}\} \in \mathbb{R}^{N \times D}\)
。多头注意力旨在从不同的视角捕捉注意力;为简单起见,选择
\(H\)
个头,每个头都是一个维度为
\(N \times \frac{D}{H}\)
的矩阵。第
\(h\)
个头的注意力矩阵
\(\mathbf{A}_h\)
可以通过以下方式计算:

\[\begin{align}
\mathbf{A}_h =
\mathrm{Softmax} \left(\frac{\mathbf{Q}_h \mathbf{K}_h^\mathsf{T}}{\sqrt{d}} \right) \in \mathbb{R}^{N \times N}.
\label{eq:attn}
\end{align}
\]

\(\mathbf{A}_h, \mathbf{Q}_h\)

\(\mathbf{K}_h\)
分别是第
\(h\)
个头的注意力矩阵、查询和键。还将值
\(\mathbf{V}\)
分割成
\(H\)
个头。为了避免由于概率分布的锐性导致的梯度消失,将
\(\mathbf{Q}_h\)

\(\mathbf{K}_h\)
的内积除以
\(\sqrt{d}\)
(
\(d = D/H\)
)。注意力矩阵被拼接为:

\[\begin{equation}
\begin{split}
\mathbf{A} &= \textrm{Concat}(\mathbf{A}_1, \cdots, \mathbf{A}_h, \cdots,\mathbf{A}_H); \\
\mathbf{V} &= \textrm{Concat}(\mathbf{V}_1, \cdots, \mathbf{V}_h, \cdots,\mathbf{V}_H).
\end{split}
\label{eq:concat}
\end{equation}
\]

在空间分割的标记之间计算的注意力,可能会引导模型关注视觉数据中最有价值的标记。随后,将加权线性聚合应用于相应的值
\(\mathbf{V}\)

\[\begin{align}
\boldsymbol{Z}^{\textrm{MHSA}} = \mathbf{AV} \in \mathbb{R}^{N \times D}.
\label{eq:val-feats}
\end{align}
\]

  • Downsampling Operation

受到
CNN
中层次架构成功的启发,一些研究将层次结构引入到
ViTs
中。这些工作将
Transformer
块划分为
\(M\)
个阶段,并在每个
Transformer
阶段之前应用下采样操作,从而减少序列长度。在论文的研究中,作者采用了一个卷积层进行下采样操作,卷积核的大小和步幅都设置为
\(2\)
。该方法允许在每个阶段灵活调整特征图的尺度,从而建立一个与人类视觉系统的组织相一致的
Transformer
层次结构。

The Less-Attention Framework

整体框架如图
1
所示。在每个阶段,分两步提取特征表示。在最初的几个
Vanilla Attention
(
VA
) 层中,进行标准的多头自注意力(
MHSA
)操作,以捕捉整体的长距离依赖关系。随后,通过对存储的注意力分数应用线性变换,模拟注意力矩阵,以减少平方计算并解决接下来的低注意力(
LA
)层中的注意力饱和问题。在这里,将第
\(m\)
个阶段的初始
\(l\)
-th VA 层的
\(\textrm{Softmax}\)
函数之前的注意力分数表示为
\(\mathbf{A}^{\text{VA},l}_m\)
,它是通过以下标准程序计算的:

\[\begin{equation}
\mathbf{A}^{\text{VA},l}_m = \frac{\mathbf{Q}^l_m(\mathbf{K}^l_m)^\mathsf{T}}{\sqrt{d}}, ~~ l \leq L^{\text{VA}}_m.
\label{eq:init}
\end{equation}
\]

这里,
\(\mathbf{Q}_m^l\)

\(\mathbf{K}_m^l\)
分别表示来自第
\(m\)
个阶段第
\(l\)
层的查询和键,遵循来自前一阶段的下采样。而
\(L^{\text{VA}}_m\)
用于表示
VA
层的数量。在最初的原始注意力阶段之后,丢弃传统的平方
MHSA
,并对
\(\mathbf{A}^\textrm{VA}_m\)
应用变换,以减少注意力计算的数量。这个过程包括进行两次线性变换,中间夹一个矩阵转置操作。为了说明,对于该阶段的第
\(l\)
层(
\(l > L^{\text{VA}}_m\)
,即
LA
层)的注意力矩阵:

\[\begin{equation}
\begin{aligned}
&\mathbf{A}^{l}_m = \Psi(\Theta(\mathbf{A}^{l-1}_m)^\mathsf{T})^\mathsf{T}, ~~ L^{\text{VA}}_m<l \leq L_m,\\
&\mathbf{Z}^{\text{LA},l} = \textrm{Softmax}(\mathbf{A}^l_m)\mathbf{V}^l.
\end{aligned}
\end{equation}
\]

在这个上下文中,
\(\Psi\)

\(\Theta\)
表示维度为
\(\mathbb{R}^{N\times{N}}\)
的线性变换层。这里,
\(L_m\)

\(L_m^{\text{VA}}\)
分别表示第
\(m\)
个阶段的层数和
VA
层的数量。在这两个线性层之间插入转置操作的目的是保持矩阵的相似性行为。这个步骤是必需的,因为单层中的线性变换是逐行进行的,这可能导致对角特性丧失。

Residual-based Attention Downsampling

当计算在分层
ViT

ViTs
)中跨阶段进行时,通常会对特征图进行下采样操作。虽然该技术减少了标记数量,但可能会导致重要上下文信息的丧失。因此,论文认为来自前一阶段学习的注意力亲和度对于当前阶段在捕捉更复杂的全局关系方面可能是有利的。受到
ResNet
的启发,后者引入了快捷连接以减轻特征饱和问题,作者在架构的下采样注意力计算中采用了类似的概念。通过引入一个短路连接,可以将固有的偏差引入当前的多头自注意力(
MHSA
)块。这使得前一阶段的注意力矩阵能够有效引导当前阶段的注意力计算,从而保留重要的上下文信息。

然而,直接将短路连接应用于注意力矩阵可能在这种情况下面临挑战,主要是由于当前阶段和前一阶段之间注意力维度的不同。为此,作者设计了一个注意力残差(
AR
)模块,该模块由深度卷积(
DWConv
)和一个
\(\textrm{Conv}_{1\times1}\)
层构成,用以在保持语义信息的同时对前一阶段的注意力图进行下采样。将前一阶段(第
\(m-1\)
阶段)的最后一个注意力矩阵(在
\(L_{m-1}\)
层)表示为
\(\textbf{A}_{m-1}^{\text{last}}\)
,将当前阶段(第
\(m\)
阶段)的下采样初始注意力矩阵表示为
\(\textbf{A}_m^\text{init}\)

\(\textbf{A}_{m-1}^{\text{last}}\)
的维度为
\(\mathbb{R}^{B\times{H}\times{N_{m-1}}\times{N_{m-1}}}\)

\(N_{m-1}\)
表示第
\(m-1\)
阶段的标记数量)。将多头维度
\(H\)
视为常规图像空间中的通道维度,因此通过
\(\textrm{DWConv}\)
操作符(
\(\textrm{stride}=2,\ \textrm{kernel size}=2\)
),可以在注意力下采样过程中捕获标记之间的空间依赖关系。经过
\(\textrm{DWConv}\)
变换后的输出矩阵适合当前阶段的注意力矩阵的尺寸,即
\(\mathbb{R}^{B\times{H}\times{N_m}\times{N_m}} (N_m = \frac{N_{m-1}}{2})\)
。在对注意力矩阵进行深度卷积后,再执行
\(\text{Conv}_{1\times1}\)
,以便在不同头之间交换信息。

论文的注意力下采样过程如图
2
所示,从
\(\textbf{A}_{m-1}^\text{last}\)

\(\textbf{A}_{m}^\text{init}\)
的变换可以表示为:

\[\begin{align}
\textbf{A}^\textrm{init}_m &= \textrm{Conv}_{1\times1}\left(\textrm{Norm}(\textrm{DWConv}(\textbf{A}^\textrm{last}_{m-1}))\right), \label{eq:residual}
\\
\mathbf{A}^{\text{VA}}_m &\gets \mathbf{A}^{\text{VA}}_m + \textrm{LS}(\textbf{A}^\textrm{init}_m) \label{eq:plus},
\end{align}
\]

其中
\(\textrm{LS}\)
是在
CaiT
中引入的层缩放操作符,用以缓解注意力饱和现象。
\(\mathbf{A}^{\text{VA}}_m\)
是第
\(m\)
阶段第一层的注意力得分,它是通过将标准多头自注意力(
MHSA
)与公式
4
和由公式
6
计算的残差相加得出的。

论文的注意力下采样模块受两个基本设计原则的指导。首先,利用
\(\text{DWConv}\)
在下采样过程中捕获空间局部关系,从而实现对注意力关系的高效压缩。其次,采用
\(\textrm{Conv}_{1\times1}\)
操作在不同头之间交换注意力信息。这一设计至关重要,因为它促进了注意力从前一阶段有效传播到后续阶段。引入残差注意力机制只需进行少量调整,通常只需在现有的
ViT
主干中添加几行代码。值得强调的是,这项技术可以无缝应用于各种版本的
Transformer
架构。唯一的前提是存储来自上一层的注意力得分,并相应地建立到该层的跳跃连接。通过综合的消融研究,该模块的重要性将得到进一步阐明。

Diagonality Preserving Loss

作者通过融入注意力变换算子,精心设计了
Transformer
模块,旨在减轻计算成本和注意力饱和的问题。然而,仍然存在一个紧迫的挑战——确保变换后的注意力保留跨
Token
之间的关系。众所周知,对注意力矩阵应用变换可能会妨碍其捕捉相似性的能力,这在很大程度上是因为线性变换以行的方式处理注意力矩阵。因此,作者设计了一种替代方法,以确保变换后的注意力矩阵保留传达
Token
之间关联所需的基本属性。一个常规的注意力矩阵应该具备以下两个属性,即对角性和对称性:

\[\begin{equation}
\begin{aligned}[b]
\mathbf{A}_{ij} &= \mathbf{A}_{ji}, \\
\mathbf{A}_{ii} &> \mathbf{A}_{ij}, \forall j \neq i.
\end{aligned}
\label{eq:property}
\end{equation}
\]

因此,设计了第
\(l\)
层的对角性保持损失,以保持这两个基本属性如下所示:

\[\begin{equation}
\begin{split}
{\mathcal{L}_{\textrm{DP},l}} &= \sum_{i=1}^N\sum_{j=1}^N\left|\mathbf{A}_{ij} -\mathbf{A}_{ji}\right| \\
&+ \sum_{i=1}^N((N-1)\mathbf{A}_{ii}-\sum_{j\neq i}\mathbf{A}_{j}).
\end{split}
\end{equation}
\]

在这里,
\(\mathcal{L}_\textrm{DP}\)
是对角性保持损失,旨在维护公式
8
中注意力矩阵的属性。在所有变换层上将对角性保持损失与普通的交叉熵 (
CE
) 损失相结合,因此训练中的总损失可以表示为:

\[\begin{equation}
\begin{aligned}[b]
\mathcal{L}_\textrm{total} &= \mathcal{L}_\textrm{CE} + \sum_{m=1}^M\sum_{l=1}^{L_m}\mathcal{L}_{\textrm{DP},l}, \\
\mathcal{L}_\textrm{CE} &= \textrm{cross-entropy}(Z_\texttt{Cls}, y),
\end{aligned}
\end{equation}
\]

其中,
\(Z_\texttt{Cls}\)
是最后一层表示中的分类标记。

Complexity Analysis

论文的架构由四个阶段组成,每个阶段包含
\(L_m\)
层。下采样层应用于每个连续阶段之间。因此,传统自注意力的计算复杂度为
\(\mathcal{O}(N_m^2{D})\)
,而相关的
K-Q-V
转换则带来了
\(\mathcal{O}(3N_mD^2)\)
的复杂度。相比之下,论文的方法在变换层内利用了
\(N_m\times N_m\)
的线性变换,从而避免了计算内积的需要。因此,变换层中注意力机制的计算复杂度降至
\(\mathcal{O}(N_m^2)\)
,实现了
\(D\)
的减少因子。此外,由于论文的方法在
Less-Attention
中只计算查询嵌入,因此
K-Q-V
转换复杂度也减少了
3
倍。

在连续阶段之间的下采样层中,以下采样率
2
为例,注意力下采样层中
DWConv
的计算复杂度可以计算为
\(\textrm{Complexity} = 2 \times 2 \times \frac{N_m}{2} \times \frac{N_m}{2} \times D = \mathcal{O}(N_m^2D)\)
。同样,注意力残差模块中
\(\textrm{Conv}_{1\times1}\)
操作的复杂度也是
\(\mathcal{O}(N_m^2D)\)
。然而,重要的是,注意力下采样在每个阶段仅发生一次。因此,对比
Less-Attention
方法所实现的复杂度减少,这些操作引入的额外复杂度可以忽略不计。

Experiments




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

work-life balance.