2024年1月

自定义注解可以标记在方法上或类上,用于在编译期或运行期进行特定的业务功能处理。在 Java 中,自定义注解使用 @interface 关键字来定义,它可以实现如:日志记录、性能监控、权限校验等功能。

在 Spring Boot 中实现一个自定义注解,可以通过 AOP(面向切面编程)或拦截器(Interceptor)来实现。

1.实现自定义注解

下面我们先使用 AOP 的方式来实现一个打印日志的自定义注解,它的实现步骤如下:

  1. 添加 Spring AOP 依赖。
  2. 创建自定义注解。
  3. 编写 AOP 拦截(自定义注解)的逻辑代码。
  4. 使用自定义注解。

具体实现如下。

① 添加 Spring AOP 依赖

在 pom.xml 中添加如下依赖:

<dependencies>
  <!-- Spring AOP dependency -->
  <dependency>
    <groupIdorg.springframework.boot</groupId>
      <artifactIdspring-boot-starter-aop</artifactId>
      </dependency>
</dependencies>

② 创建自定义注解

创建一个新的 Java 注解类,通过 @interface 关键字来定义,并可以添加元注解以及属性。

import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomLogAnnotation {
    String value() default "";
    boolean enable() default true;
}

在上面的例子中,我们定义了一个名为 CustomLogAnnotation 的注解,它有两个属性:value 和 enable,分别设置了默认值。

  • @Target(ElementType.METHOD) 指定了该注解只能应用于方法级别。
  • @Retention(RetentionPolicy.RUNTIME) 表示这个注解在运行时是可见的,这样 AOP 代理才能在运行时读取到这个注解。

③ 编写 AOP 拦截(自定义注解)的逻辑代码

使用 Spring AOP 来拦截带有自定义注解的方法,并在其前后执行相应的逻辑。

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class CustomLogAspect {
    @Around("@annotation(customLog)")
    public Object logAround(ProceedingJoinPoint joinPoint, CustomLogAnnotation customLog) throws Throwable {
        if (customLog.enable()) {
            // 方法执行前的处理
            System.out.println("Before method execution: " + joinPoint.getSignature().getName());
            long start = System.currentTimeMillis();
            // 执行目标方法
            Object result = joinPoint.proceed();
            // 方法执行后的处理
            long elapsedTime = System.currentTimeMillis() - start;
            System.out.println("After method execution (" + elapsedTime + 
                               "ms): " + customLog.value());
            return result;
        } else {
            return joinPoint.proceed();
        }
    }
}

④ 使用自定义注解

将自定义注解应用于需要进行日志记录的方法上,如下代码所示:

@RestController
public class MyController {
    @CustomLogAnnotation(value = "This is a test method", enable = true)
    @GetMapping("/test")
    public String testMethod() {
        // 业务逻辑代码
        return "Hello from the annotated method!";
    }
}

2.实际工作中的自定义注解

实际工作中我们通常会使用自定义注解来实现如权限验证,或者是幂等性判断等功能。

幂等性判断是指在分布式系统或并发环境中,对于同一操作的多次重复请求,系统的响应结果应该是一致的。简而言之,无论接收到多少次相同的请求,系统的行为和结果都应该是相同的。

3.如何实现自定义幂等性注解?

下面我们使用拦截器 + Redis 的方式来实现一下自定义幂等性注解,它的实现步骤如下:

  1. 创建自定义幂等性注解。
  2. 创建拦截器,实现幂等性逻辑判断。
  3. 配置拦截规则。
  4. 使用自定义幂等性注解。

具体实现如下。

① 创建自定义幂等性注解

@Retention(RetentionPolicy.RUNTIME) // 程序运行时有效
@Target(ElementType.METHOD) // 方法注解
public @interface Idempotent {
    /**
     * 请求标识符的参数名称,默认为"requestId"
     */
    String requestId() default "requestId";
    /**
     * 幂等有效时长(单位:秒)
     */
    int expireTime() default 60;
}

② 创建拦截器

@Component
public class IdempotentInterceptor extends HandlerInterceptorAdapter {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Method method = ((HandlerMethod) handler).getMethod();
        Idempotent idempotent = method.getAnnotation(Idempotent.class);
        if (idempotent != null) {
            // 获取请求中的唯一标识符
            String requestId = obtainRequestId(request, idempotent.requestId());
            // 判断该请求是否已经处理过
            if (redisTemplate.opsForValue().get(idempotentKey(requestId)) != null) {
                // 已经处理过,返回幂等响应
                response.getWriter().write("重复请求");
                return false;
            } else {
                // 将请求标识符存入Redis,并设置过期时间
                redisTemplate.opsForValue().set(idempotentKey(requestId), "processed", idempotent.expireTime(), TimeUnit.SECONDS);
                return true; // 继续执行业务逻辑
            }
        }
        return super.preHandle(request, response, handler);
    }

    private String idempotentKey(String requestId) {
        return "idempotent:" + requestId;
    }

    private String obtainRequestId(HttpServletRequest request, String paramName) {
        // 实现从请求中获取唯一标识符的方法
        return request.getParameter(paramName);
    }
}

③ 配置拦截器

在 Spring Boot 配置文件类中,添加拦截器配置:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private IdempotentInterceptor idempotentInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(idempotentInterceptor)
        	.addPathPatterns("/**"); // 拦截所有接口
    }
}

④ 使用自定义注解

最后,在需要进行幂等控制的 Controller 方法上使用 @Idempotent 注解:

Java
@RestController
public class TestController {
    @PostMapping("/order")
    @Idempotent(requestId = "orderId") // 假设orderId是从客户端传来的唯一标识订单请求的参数
    public String placeOrder(@RequestParam("orderId") String orderId, ...) {
        // 业务处理逻辑
    }
}

这样,当有相同的请求 ID 在指定的有效期内再次发起请求时,会被拦截器识别并阻止其重复执行业务逻辑。

小结

自定义注解被广泛应用于日常开发中,像日志记录、性能监控、权限判断和幂等性判断等功能的实现,使用自定义注解来实现是非常方便的。在 Spring Boot 中,使用 @interface 关键字来定义自定义注解,之后再使用 AOP 或拦截器的方式实现自定义注解,之后就可以方便的使用自定义注解了。

课后思考

那么问题来了,AOP 和拦截器的底层实现原理是啥呢?欢迎评论区留言互动。点赞超过 20,更新下篇文章。

本文已收录到我的面试小站
www.javacn.site
,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。

支持向量机
也是一种既可以处理
分类
问题,也可以处理
回归
问题的算法。
关于支持向量机在
回归问题上
的应用,请参考:
TODO

支持向量机分类
广泛应用于图像识别、文本分类、生物信息学(例如基因分类)、手写数字识别等领域。

1. 算法概述

支持向量机
的主要思想是找到一个超平面,将不同类别的样本最大化地分隔开。
超平面的位置由支持向量决定,它们是离分隔边界最近的数据点。
对于二分类问题,SVM寻找一个超平面,使得正例和支持向量到超平面的距离之和等于反例和支持向量到超平面的距离之和。
如果这个等式不成立,SVM将寻找一个更远离等式中不利样本的超平面。

下面的示例,演示了支持
向量机分类
算法在
图像识别
上的应用。

2. 创建样本数据

这次的样本使用的是
scikit-learn
自带的
手写数字数据集

import matplotlib.pyplot as plt
from sklearn import datasets

# 加载手写数据集
data = datasets.load_digits()

_, axes = plt.subplots(nrows=2, ncols=4, figsize=(10, 6))
for ax, image, label in zip(np.append(axes[0], axes[1]), data.images, data.target):
    ax.set_axis_off()
    ax.imshow(image, cmap=plt.cm.gray_r, interpolation="nearest")
    ax.set_title("目标值: {}".format(label))

image.png
这里显示了其中的几个手写数字,这个数据集总共有大约
1700多个
手写数字。

3. 模型训练

样本数据中,手写数字的图片存储为一个
8x8
的二维数组。
比如:

data.images[0]

# 运行结果
array([[ 0.,  0.,  5., 13.,  9.,  1.,  0.,  0.],
       [ 0.,  0., 13., 15., 10., 15.,  5.,  0.],
       [ 0.,  3., 15.,  2.,  0., 11.,  8.,  0.],
       [ 0.,  4., 12.,  0.,  0.,  8.,  8.,  0.],
       [ 0.,  5.,  8.,  0.,  0.,  9.,  8.,  0.],
       [ 0.,  4., 11.,  0.,  1., 12.,  7.,  0.],
       [ 0.,  2., 14.,  5., 10., 12.,  0.,  0.],
       [ 0.,  0.,  6., 13., 10.,  0.,  0.,  0.]])

所以,在分割
训练集

测试集
之前,我们需要先将手写数字的的存储格式从
8x8

二维数组
转换为
64x1

一维数组

from sklearn.model_selection import train_test_split

n_samples = len(data.images)
X = data.images.reshape((n_samples, -1))
y = data.target

# 分割训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1)

按照
9:1的比例
来划分训练集和测试集。

然后用
scikit-learn
中的
SVC
模型来训练样本:

from sklearn.svm import SVC

# 定义
reg = SVC()

# 训练模型
reg.fit(X_train, y_train)

模型的训练效果:

# 在测试集上进行预测
y_pred = reg.predict(X_test)

correct_pred = np.sum(y_pred == y_test)
print("预测正确率:{:.2f}%".format(correct_pred / len(y_pred) * 100))

# 运行效果
预测正确率:98.89%

正确率非常高,下面我们看看没识别出来的手写数字是哪些。

wrong_pred = []
for i in range(len(y_pred)):
    if y_pred[i] != y_test[i]:
        wrong_pred.append(i)

print(wrong_pred)

# 运行效果
[156, 158]

在测试集中,只有两个手写数字识别错了。

我面看看识别错的2个手写数字是什么样的:

_, axes = plt.subplots(nrows=1, ncols=2, figsize=(8, 3))
for i in range(2):
    idx = wrong_pred[i]
    image = X_test[idx].reshape(8, 8)
    axes[i].set_axis_off()
    axes[i].imshow(image, cmap=plt.cm.gray_r, interpolation="nearest")
    axes[i].set_title("预测值({})  目标值({})".format(y_pred[idx], y_test[idx]))

image.png
可以看出,即使人眼去识别,这两个手写数字也不太容易识别。

4. 总结

支持向量机分类算法的优势有:

  1. 有效处理高维数据
    :对高维数据非常有效,即使在数据维度超过样本数量的情况下也能工作得很好。
  2. 高效
    :只使用一部分训练数据(即支持向量)来做决策,这使得算法更加内存高效。
  3. 稳定性较好
    :由于其决策边界取决于支持向量而不是所有的数据点,因此模型的稳定性较好,对噪声和异常值的敏感度较低。

它的劣势主要有:

  1. 对参数和核函数敏感
    :性能高度依赖于参数设置(如惩罚参数C和核函数的选择)。如果参数选择不当,可能会导致过拟合或欠拟合。
  2. 难以解释
    :不像决策树那样直观,难以理解和解释。
  3. 处理大规模数据时速度较慢
    :训练过程涉及到二次规划问题,需要使用复杂的优化算法,因此在处理大规模数据时可能较慢。

基于ChatGLM-6B第一版,要注意还有ChatGLM2-6B以及ChatGLM3-6B

转载请备注出处:
https://www.cnblogs.com/zhiyong-ITNote/

ChatGLMPreTrainedModel

官方的描述是 处理权重初始化的抽象类,以及下载和加载预训练模型的接口。

掩码

如下是GLM模型的掩码结构,在此抽象类中,由
get_masks
函数处理
image.png


# 转载请备注出处:https://www.cnblogs.com/zhiyong-ITNote/

def get_masks(input_ids, device):
    batch_size, seq_length = input_ids.shape
    # bos_token_id所在的位置
    context_lengths = [seq.tolist().index(130004) for seq in input_ids]
    attention_mask = torch.ones((batch_size, seq_length, seq_length), device=device)
    # 填充下三角全为1,上三角全为0
    attention_mask.tril_()
    # 遍历每个序列直到bos_token_id出现的位置,更新掩码,改为双向注意力
    for i, context_length in enumerate(context_lengths):
        attention_mask[i, :, :context_length] = 1
    # 扩充维度
    attention_mask.unsqueeze_(1)
    # 变更为True和False的维度形式
    attention_mask = (attention_mask < 0.5).bool()

    return attention_mask

位置编码

GLM模型中位置编码是2D的,有两层的位置表示,分别是序列的位置表示和mask block的位置表示。由
get_position_ids
函数处理。position_ids对应GLM论文中的postion 1,block_position_ids对应GLM论文中的position 2。

def get_position_ids(self, input_ids, mask_positions, device, use_gmasks=None):
    """
    input_ids: [batch_size, seq_length]
    mask_positions: [batch_size],由于GLM系列中会使用[Mask]或[gMask]标志,mask_positions就是指这些标注的具体位置
    """
    batch_size, seq_length = input_ids.shape
    if use_gmasks is None:
        use_gmasks = [False] * batch_size
    # context_lengths:未被padding前,batch中各个样本的长度
    context_lengths = [seq.tolist().index(self.config.bos_token_id) for seq in input_ids]
    # 2维位置编码
    if self.position_encoding_2d:
        # [0,1,2,...,seq_length-1]
        position_ids = torch.arange(seq_length, dtype=torch.long, device=device).unsqueeze(0).repeat(batch_size, 1)
        # 将原始输入后所有位置的postion id都设置为[Mask]或者[gMask]的位置id
        for i, context_length in enumerate(context_lengths):
            position_ids[i, context_length:] = mask_positions[i]
        # 原始输入的位置编码全部设置为0,待生成的位置添加顺序的位置id
        # 例如:[0,0,0,0,1,2,3,4,5]
        block_position_ids = [torch.cat((
            torch.zeros(context_length, dtype=torch.long, device=device),
            torch.arange(seq_length - context_length, dtype=torch.long, device=device) + 1
        )) for context_length in context_lengths]
        block_position_ids = torch.stack(block_position_ids, dim=0)
        # 将postion_ids和block_position_ids堆叠在一起,用于后续的参数传入;
        # 在注意力层中,还有将这个position_ids拆分为两部分: position_ids, block_position_ids
        position_ids = torch.stack((position_ids, block_position_ids), dim=1)
    else:
        position_ids = torch.arange(seq_length, dtype=torch.long, device=device).unsqueeze(0).repeat(batch_size, 1)
        for i, context_length in enumerate(context_lengths):
            if not use_gmasks[i]:
                position_ids[i, context_length:] = mask_positions[i]

    return position_ids

ChatGLMModel

该Model通过组装各个组件构造最终的模型结构。模型的微调处理也是在这里进行。

转载请备注出处:
https://www.cnblogs.com/zhiyong-ITNote/

class ChatGLMModel(ChatGLMPreTrainedModel):
    """
    The model can behave as an encoder (with only self-attention) as well
    as a decoder, in which case a layer of cross-attention is added between
    the self-attention layers, following the architecture described in [Attention is
    all you need](https://arxiv.org/abs/1706.03762) by Ashish Vaswani,
    Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N. Gomez, Lukasz Kaiser and Illia Polosukhin.
    To behave as an decoder the model needs to be initialized with the
    `is_decoder` argument of the configuration set to `True`.
    To be used in a Seq2Seq model, the model needs to initialized with both `is_decoder`
    argument and `add_cross_attention` set to `True`; an
    `encoder_hidden_states` is then expected as an input to the forward pass.
    """

    def __init__(self, config: ChatGLMConfig, empty_init=True):
        super().__init__(config)
        if empty_init:
            init_method = skip_init
        else:
            init_method = default_init
        # recording parameters
        self.max_sequence_length = config.max_sequence_length
        self.hidden_size = config.hidden_size
        self.params_dtype = torch.half
        self.num_attention_heads = config.num_attention_heads
        self.vocab_size = config.vocab_size
        self.num_layers = config.num_layers
        self.layernorm_epsilon = config.layernorm_epsilon
        self.inner_hidden_size = config.inner_hidden_size
        self.hidden_size_per_attention_head = self.hidden_size // self.num_attention_heads
        self.position_encoding_2d = config.position_encoding_2d
        self.pre_seq_len = config.pre_seq_len
        self.prefix_projection = config.prefix_projection

        self.word_embeddings = init_method(
            torch.nn.Embedding,
            num_embeddings=self.vocab_size, embedding_dim=self.hidden_size,
            dtype=self.params_dtype
        )
        self.gradient_checkpointing = False

        # 返回transform结构的GLMBlock
        def get_layer(layer_id):
            return GLMBlock(
                self.hidden_size,
                self.num_attention_heads,
                self.layernorm_epsilon,
                layer_id,
                inner_hidden_size=self.inner_hidden_size,
                hidden_size_per_attention_head=self.hidden_size_per_attention_head,
                layernorm=LayerNorm,
                use_bias=True,
                params_dtype=self.params_dtype,
                position_encoding_2d=self.position_encoding_2d,
                empty_init=empty_init
            )
        # 堆叠GLMBlock,参数就是config.json中指定的num_layers,默认堆叠28层
        self.layers = torch.nn.ModuleList(
            [get_layer(layer_id) for layer_id in range(self.num_layers)]
        )

        # 输出之前做最后一次的层归一化
        self.final_layernorm = LayerNorm(self.hidden_size, eps=self.layernorm_epsilon)

        # 处理微调,pre_seq_len参数来自微调脚本train.sh的PRE_SEQ_LEN参数
        if self.pre_seq_len is not None:
            for param in self.parameters():
                param.requires_grad = False
            self.prefix_tokens = torch.arange(self.pre_seq_len).long()
            self.prefix_encoder = PrefixEncoder(config)
            self.dropout = torch.nn.Dropout(0.1)

            # total_params = sum(p.numel() for p in self.parameters())
            # trainable_params = sum(p.numel() for p in self.parameters() if p.requires_grad)
            # print("Using p-tuning v2: # trainable_params = {} / {}".format(trainable_params, total_params))

    def get_input_embeddings(self):
        return self.word_embeddings

    def set_input_embeddings(self, new_embeddings: torch.Tensor):
        self.word_embeddings = new_embeddings

    def get_prompt(self, batch_size, device, dtype=torch.half):
        prefix_tokens = self.prefix_tokens.unsqueeze(0).expand(batch_size, -1).to(device)
        past_key_values = self.prefix_encoder(prefix_tokens).type(dtype)
        past_key_values = past_key_values.view(
            batch_size,
            self.pre_seq_len,
            self.num_layers * 2,
            self.num_attention_heads,
            self.hidden_size // self.num_attention_heads
        )
        # seq_len, b, nh, hidden_size
        past_key_values = self.dropout(past_key_values)
        past_key_values = past_key_values.permute([2, 1, 0, 3, 4]).split(2)
        # past_key_values = [(v[0], v[1]) for v in past_key_values]
        return past_key_values

    @add_start_docstrings_to_model_forward(CHATGLM_6B_INPUTS_DOCSTRING.format("batch_size, sequence_length"))
    @add_code_sample_docstrings(
        checkpoint=_CHECKPOINT_FOR_DOC,
        output_type=BaseModelOutputWithPastAndCrossAttentions,
        config_class=_CONFIG_FOR_DOC,
    )
    def forward(
            self,
            input_ids: Optional[torch.LongTensor] = None,
            position_ids: Optional[torch.LongTensor] = None,
            attention_mask: Optional[torch.Tensor] = None,
            past_key_values: Optional[Tuple[Tuple[torch.Tensor, torch.Tensor], ...]] = None,
            inputs_embeds: Optional[torch.LongTensor] = None,
            use_cache: Optional[bool] = None,
            output_attentions: Optional[bool] = None,
            output_hidden_states: Optional[bool] = None,
            return_dict: Optional[bool] = None,
    ) -> Union[Tuple[torch.Tensor, ...], BaseModelOutputWithPast]:

        output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
        output_hidden_states = (
            output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
        )
        use_cache = use_cache if use_cache is not None else self.config.use_cache
        return_dict = return_dict if return_dict is not None else self.config.use_return_dict

        if self.gradient_checkpointing and self.training:
            if use_cache:
                logger.warning_once(
                    "`use_cache=True` is incompatible with gradient checkpointing. Setting `use_cache=False`..."
                )
                use_cache = False

        if input_ids is not None and inputs_embeds is not None:
            raise ValueError("You cannot specify both input_ids and inputs_embeds at the same time")
        elif input_ids is not None:
            batch_size, seq_length = input_ids.shape[:2]
        elif inputs_embeds is not None:
            batch_size, seq_length = inputs_embeds.shape[:2]
        else:
            raise ValueError("You have to specify either input_ids or inputs_embeds")

        # embedding层
        if inputs_embeds is None:
            inputs_embeds = self.word_embeddings(input_ids)

        if past_key_values is None:
            if self.pre_seq_len is not None:
                past_key_values = self.get_prompt(batch_size=input_ids.shape[0], device=input_ids.device,
                                                  dtype=inputs_embeds.dtype)
            else:
                past_key_values = tuple([None] * len(self.layers))

            # 获得注意力mask
            if attention_mask is None:
                attention_mask = self.get_masks(
                    input_ids,
                    device=input_ids.device
                )


            # 处理位置编码
            if position_ids is None:
                MASK, gMASK = self.config.mask_token_id, self.config.gmask_token_id
                seqs = input_ids.tolist()
                # 记录input_ids中是否使用了mask以及mask的位置
                # mask_positions记录每个样本中mask的位置
                # use_gmasks记录是否使用了gMask
                mask_positions, use_gmasks = [], []
                for seq in seqs:
                    mask_token = gMASK if gMASK in seq else MASK
                    use_gmask = mask_token == gMASK
                    mask_positions.append(seq.index(mask_token))
                    use_gmasks.append(use_gmask)
                # 获取位置编码
                position_ids = self.get_position_ids(
                    input_ids,
                    mask_positions=mask_positions,
                    device=input_ids.device,
                    use_gmasks=use_gmasks
                )

        # 微调的处理
        if self.pre_seq_len is not None and attention_mask is not None:
            prefix_attention_mask = torch.ones(batch_size, 1, input_ids.size(-1), self.pre_seq_len).to(
                attention_mask.device)
            prefix_attention_mask = (prefix_attention_mask < 0.5).bool()
            attention_mask = torch.cat((prefix_attention_mask, attention_mask), dim=3)

        # [seq_len, batch, hidden_size]
        hidden_states = inputs_embeds.transpose(0, 1)

        presents = () if use_cache else None
        all_self_attentions = () if output_attentions else None
        all_hidden_states = () if output_hidden_states else None

        if attention_mask is None:
            attention_mask = torch.zeros(1, 1, device=input_ids.device).bool()
        else:
            attention_mask = attention_mask.to(hidden_states.device)

        # 遍历堆叠的transform层,并开始执行
        for i, layer in enumerate(self.layers):

            if output_hidden_states:
                all_hidden_states = all_hidden_states + (hidden_states,)
            layer_past = past_key_values[i]

            if self.gradient_checkpointing and self.training:
                layer_ret = torch.utils.checkpoint.checkpoint(
                    layer,
                    hidden_states,
                    position_ids,
                    attention_mask,
                    torch.tensor(i),
                    layer_past,
                    use_cache,
                    output_attentions
                )
            else:
                layer_ret = layer(
                    hidden_states,
                    position_ids=position_ids,
                    attention_mask=attention_mask,
                    layer_id=torch.tensor(i),
                    layer_past=layer_past,
                    use_cache=use_cache,
                    output_attentions=output_attentions
                )

            hidden_states = layer_ret[0]

            if use_cache:
                presents = presents + (layer_ret[1],)

            if output_attentions:
                all_self_attentions = all_self_attentions + (layer_ret[2 if use_cache else 1],)

        # Final layer norm.
        hidden_states = self.final_layernorm(hidden_states)

        if output_hidden_states:
            all_hidden_states = all_hidden_states + (hidden_states,)

        if not return_dict:
            return tuple(v for v in [hidden_states, presents, all_hidden_states, all_self_attentions] if v is not None)

        return BaseModelOutputWithPast(
            last_hidden_state=hidden_states,
            past_key_values=presents,
            hidden_states=all_hidden_states,
            attentions=all_self_attentions,
        )

其完整结构如下所示。相比较传统的Transformer模型结构,ChatGLM模型中,将GLMBlock统一了两者,只需要增加
is_decoder=true
即可切换为decoder行为,在ChatGLMModel源码的注释中就已经写清楚了,默认是encoder;GLU层对应Transformer模型的FFN层。

转载请备注出处:
https://www.cnblogs.com/zhiyong-ITNote/

在前面随笔《
使用Winform开发自定义用户控件,以及实现相关自定义事件的处理
》中介绍了Winform用户自定义控件的处理,对于Winform自定义的用户控件来说,它的呈现方式主要就是基于GDI+进行渲染的,对于数量不多的控件呈现,一般不会觉察性能有太多的问题,随着控件的数量大量的增加,就会产生性能问题,比较缓慢,或者句柄创建异常等问题。本篇随笔介绍WPF技术处理的自定义用户控件,引入虚拟化技术的处理,较好的解决这些问题。

前面例子我测试一次性在界面呈现的控件总数接近2k左右的时候,句柄就会创建异常。由于Winform控件没有引入虚拟化技术来重用UI控件的资源,因此控件呈现量多的话,就会有严重的性能问题。而WPF引入的虚拟化技术后,对于UI资源的重用就会降低界面的消耗,而且即使数量再大,也不会有卡顿的问题。其原理就是UI变化还是那些内容,触发滚动的时候,也只是对可见控件的数据进行更新,从而大量减少UI控件创建刷新的消耗。

如果接触过IOS开发的时候,它们的处理也是一样,在介绍列表处理绑定的时候,它本身就强制重用列表项的资源,从而达到降低UI资源消耗 的目的。

1、WPF对于简单的用户控件和虚拟化的处理

我们来介绍自定义控件之前,我们先来了解一下虚拟化的技术处理。

在WPF应用程序开发过程中,大数据量的数据展现通常都要考虑性能问题。

例如对于WPF程序来说,原始数据源数据量很大,但是某一时刻数据容器中的可见元素个数是有限的,剩余大多数元素都处于不可见状态,如果一次性将所有的数据元素都渲染出来则会非常的消耗性能。因而可以考虑只渲染当前可视区域内的元素,当可视区域内的元素需要发生改变时,再渲染即将展现的元素,最后将不再需要展现的元素清除掉,这样可以大大提高性能。

WPF列表控件提供的最重要功能是UI虚拟化(UI Virtaulization),UI 虚拟化是列表仅为当前显示项创建容器对象的一种技术。

在WPF中System.Windows.Controls命名空间下的VirtualizingStackPanel可以实现数据展现的虚拟化功能,ListBox的默认元素展现容器就是它。但有时VirtualizingStackPanel的布局并不能满足我们的实际需要,此时就需要实现自定义布局的虚拟容器了。

要想实现一个虚拟容器,并让虚拟容器正常工作,必须满足以下两个条件:

1、容器继承自System.Windows.Controls.VirtualizingPanel,并实现子元素的实例化、虚拟化及布局处理。

2、虚拟容器要做为一个System.Windows.Controls.ItemsControl(或继承自ItemsControl的类)实例的ItemsPanel(实际上是定义一个ItemsPanelTemplate)

我在这里首先介绍如何使用虚拟化容器控件即可,自定义的处理可以在熟悉后,参考一些代码进行处理即可。

VirtualizingPanel从一开始就存在于 WPF 中。这提供了不必立即为可视化创建ItemsControl的所有 UI 元素的可能性。

VirtualizingPanel类中实现以下几项依赖属性。

  1. CacheLength/CacheLengthUnit
  2. IsContainerVirtualizable
  3. IsVirtualizing
  4. IsVirtualizingWhenGrouping
  5. ScrollUnit
  6. VirtualizationMode

VirtualizingPanel 可以通过CacheLengthUnit 设置缓存单元。可能的有:Item、Page、Pixel 几个不同的项目,这确定了视口之前和之后的缓存大小。这样可以避免 UI 元素只在可见时才生成。

例如对于ListBox控件的虚拟化处理,代码如下所示。

<ListBox ItemsSource="{Binding VirtualizedBooks}"ItemTemplate="{StaticResource BookTemplate}"VirtualizingPanel.IsVirtualizing="True"VirtualizingPanel.CacheLength="1,2"VirtualizingPanel.CacheLengthUnit="Page"/>

在我之前的WPF相关随笔中,我介绍过UI部分,采用了lepoco/wpfui 的项目界面来集成处理的。

GitHub地址:
https://github.com/lepoco/wpfui

文档地址:
https://wpfui.lepo.co/documentation/

lepoco/wpfui 的项目控件组中也提供了一个类似流式布局(类似Winform的FlowLayoutPanel)的虚拟化控件VirtualizingItemsControl,比较好用,我们借鉴来介绍一下。

<ui:VirtualizingItemsControlForeground="{DynamicResource TextFillColorSecondaryBrush}"ItemsSource="{Binding ViewModel.Colors, Mode=OneWay}"VirtualizingPanel.CacheLengthUnit="Item">
   <ItemsControl.ItemTemplate>
       <DataTemplateDataType="{x:Type models:DataColor}">
           <ui:ButtonWidth="80"Height="80"Margin="2"Padding="0"HorizontalAlignment="Stretch"VerticalAlignment="Stretch"Appearance="Secondary"Background="{Binding Color, Mode=OneWay}"FontSize="25"Icon="Fluent24" />
       </DataTemplate>
   </ItemsControl.ItemTemplate>
</ui:VirtualizingItemsControl>

这个界面的效果如下所示,它的后端ViewModel的数据模型中绑定9k左右个记录对象,而在UI虚拟化的加持下,滚动处理没有任何卡顿,这就是其虚拟化优势所在。

我们上面为了简单介绍呈现的效果,主要在模板里面放置了一个简单的按钮控件来定义颜色块,我们开发的界面往往相对会复杂一些,如果我们不太考虑重用界面元素,简单的对象组装可以在这个 DataTemplate 模板里面进行处理,如下代码所示。

<ui:VirtualizingItemsControlForeground="{DynamicResource TextFillColorSecondaryBrush}"ItemsSource="{Binding ViewModel.Colors, Mode=OneWay}"VirtualizingPanel.CacheLengthUnit="Item">
    <ItemsControl.ItemTemplate>
        <DataTemplateDataType="{x:Type models:DataColor}">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinitionHeight="auto" />
                    <RowDefinitionHeight="50" />
                </Grid.RowDefinitions>
                <ui:ButtonGrid.Row="0"Width="80"Height="80"Margin="2"Padding="0"HorizontalAlignment="Stretch"VerticalAlignment="Stretch"Appearance="Secondary"Background="{Binding Color, Mode=OneWay}"FontSize="25"Icon="Fluent24" />
                <GridGrid.Row="1">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinitionWidth="20*" />
                        <ColumnDefinitionWidth="20*" />
                    </Grid.ColumnDefinitions>
                    <TextBlockGrid.Column="0"FontWeight="Bold"Foreground="Red"Text="左侧"TextAlignment="Center" />
                    <TextBlockGrid.Column="1"FontWeight="Black"Foreground="Blue"Text="右侧"TextAlignment="Center" />
                </Grid>

            </Grid>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ui:VirtualizingItemsControl>

通过我们自定义的Grid布局,很好的组织起来相关的自定义控件的界面效果,会得到项目的界面效果。

2、WPF自定义控件的处理

前面介绍了一些基础的虚拟化控件容器和一些常规的自定义控件内容的只是,我们在开发桌面程序的时候,为了方便重用等原因,我们往往把一些复杂的界面元素逐层分解,组合成一些自定义的控件,然后组装层更高级的自定义控件,这样就可以构建界面和逻辑比较复杂的一些界面元素了。

在前面随笔《
使用Winform开发自定义用户控件,以及实现相关自定义事件的处理
》中介绍了Winform用户自定义控件的处理,其实WPF的处理思路也是类似,只是具体细节有所差异而已。

前面文章中介绍,为了使用户控件更加规范化,我们可以定义一个接口,声明相关的属性和处理方法,如下代码所示。(这部分WPF和Winform自定义控件开发一样处理)

/// <summary>
    ///自定义控件的接口/// </summary>
    public interfaceINumber
{
/// <summary> ///数字/// </summary> string Number { get; set; }/// <summary> ///数值颜色/// </summary> Color Color { get; set; }/// <summary> ///显示文本/// </summary> string Animal { get; set; }/// <summary> ///显示文本/// </summary> string WuHan { get; set; }/// <summary> ///设置选中的内容的处理/// </summary> /// <param name="data">事件数据</param> voidSetSelected(ClickEventData data);
}

和WInform开发一样,WPF也是创建一个自定义的控件,在项目上右键添加自定义控件,如下界面所示。

我们同样命名为NumberItem,最终后台Xaml的C#代码生成如下所示(我们让它继承接口 INumber )。

    /// <summary>
    ///NumberItem.xaml 的交互逻辑/// </summary>
    public partial class NumberItem : UserControl, INumber

WPF自定义控件实现接口的属性定义,不是简单的处理,需要按照WPF的属性处理规则,这里和Winform处理有些小差异。

    /// <summary>
    ///NumberItem.xaml 的交互逻辑/// </summary>
    public partial classNumberItem : UserControl, INumber
{
#region 控件属性定义 /// <summary> ///数字/// </summary> public stringNumber
{
get { return (string)GetValue(NumberProperty); }set{ SetValue(NumberProperty, value); }
}
/// <summary> ///颜色/// </summary> publicColor Color
{
get { return(Color)GetValue(ColorProperty); }set{ SetValue(ColorProperty, value); }
}
/// <summary> ///显示文本/// </summary> public stringAnimal
{
get { return (string)GetValue(AnimalProperty); }set{ SetValue(AnimalProperty, value); }
}
/// <summary> ///显示文本/// </summary> public stringWuHan
{
get { return (string)GetValue(WuHanProperty); }set{ SetValue(WuHanProperty, value); }
}
public static readonly DependencyProperty ColorProperty =DependencyProperty.Register(
nameof(Color),
typeof(Color), typeof(NumberItem), newFrameworkPropertyMetadata(Colors.Transparent, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));public static readonly DependencyProperty NumberProperty =DependencyProperty.Register(
nameof(Number),
typeof(string), typeof(NumberItem), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, newPropertyChangedCallback(OnNumberPropertyChanged)));public static readonly DependencyProperty AnimalProperty =DependencyProperty.Register(
nameof(Animal),
typeof(string), typeof(NumberItem), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));public static readonly DependencyProperty WuHanProperty =DependencyProperty.Register(
nameof(WuHan),
typeof(string), typeof(NumberItem), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));#endregion

我们可以看到属性名称的取值和赋值,通过GetValue、SetValue 的操作实现,同时需要定义一个静态变量 DependencyProperty 的属性定义,如 ***Property。

这个是WPF属性的常规处理,没增加一个属性名称,就增加一个对应类型DependencyProperty 的**Property,如下所示。

public static readonly DependencyPropertyColorProperty =DependencyProperty.Register(
nameof(Color),
typeof(Color), typeof(NumberItem), new FrameworkPropertyMetadata(Colors.Transparent, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

自定义控件的事件通知,有两种处理方法,可以通过常规事件的冒泡层层推送到界面顶端处理,也可以使用MVVM的消息通知(类似消息总线的处理),我们先来介绍MVVM的消息通知,因为它最为简单易用。

而这里所说的MVVM包,是指微软的 CommunityToolkit.Mvvm的组件包,有兴趣可以全面了解一下。

CommunityToolkit.Mvvm
(又名 MVVM 工具包,以前名为
Microsoft.Toolkit.Mvvm
) 是一个现代、快速且模块化的 MVVM 库。 它是 .NET 社区工具包的一部分,围绕以下原则构建:



  • 平台和运行时独立 - .NET Standard 2.0、 .NET Standard 2.1 和 .NET 6

前言

英特尔公司发行的模型部署工具OpenVINO™模型部署套件,可以实现在不同系统环境下运行,且发布的OpenVINO™ 2023最新版目前已经支持MacOS系统并同时支持在苹果M系列芯片上部署模型。在该项目中,我们将向大家展示如何在MacOS系统、M2芯片的Macbook Air电脑上,展示使用OpenVINO™ C++ API 部署深度学习模型。

1. OpenVINO™

英特尔发行版 OpenVINO™ 工具套件基于 oneAPI 而开发,可以加快高性能计算机视觉和深度学习视觉应用开发速度工具套件,适用于从边缘到云的各种英特尔平台上,帮助用户更快地将更准确的真实世界结果部署到生产系统中。通过简化的开发工作流程,OpenVINO™ 可赋能开发者在现实世界中部署高性能应用程序和算法。

image

OpenVINO™ 2023.2 于 2023 年 11 月 16 日发布,该工具包带来了挖掘生成人工智能全部潜力的新功能。更多的生成式 AI 覆盖和框架集成,以最大限度地减少代码更改,并且扩展了对直接 PyTorch 模型转换的模型支持。支持更多新的模型,包括 LLaVA、chatGLM、Bark 和 LCM 等著名模型。支持更广泛的大型语言模型(LLM)和更多模型压缩技术,支持运行时推理支持以下 Int4 模型压缩格式,通过神经网络压缩框架(NNCF) 进行本机 Int4 压缩等一系列新的功能。

image

通过OpenVINO™官网信息,我们可以看出,目前OpenVINO™已经能够在苹果MacOS系统、M系列芯片上运行,这为使用MacOS系统的开发者提供了很好的工具。因此在此处,我们将在MacOS系统、M2芯片的Macbook Air电脑上,展示使用 OpenVINO™ C++ API 部署深度学习模型的详细流程。

2. OpenVINO™ 下载

官方在发布版本中已经提供MacOS系统的编译库,因此在此处我们只需要下载官方编译库即可

首先访问
OpenVINO™
网站,依次选择版本号、操作系统、安装方式等内容,然后点击下载,如下图所示:

image

下面是官方编译的文件,此处主要提供了两个版本,一个是适用于苹果电脑之前的版本,主要是MacOS 10以及之前的版本系统并且使用的是Intel CPU,另一个是使用了苹果的M系列芯片的新版本电脑,主要是MacOS 11 之后的系统。大家可以根据自己的电脑进行选择:

image

下载完后,将该文件解压到任意文件夹,在此处为了方便后续使用一集更新,将其解压到用户文件夹,如下图所示:

image

后续我们会使用CMake进行项目编译,因此我们此处无需再做其他的设置。

其他环境配置:

  • MacOS:14.2.1
  • CMake:3.28
  • Make:3.81
  • 编译软件:Visual Studio Code
  • OpenCV:4.8.0
    其他环境配置此处不做过多赘述,OpenCV环境安装可以参考下述文章实现:

3. 代码实现

此处我们以Yolov8图片分类模型为例进行项目测试,由于该模型之前我们已经多次使用,所以在此处不在做耕作的阐述,具体代码如下所示:

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <sys/time.h>
#include "openvino/openvino.hpp" //openvino header file
#include "opencv2/opencv.hpp"    //opencv header file

int main(int argc, char* argv[])
{
    ov::Version version = ov::get_openvino_version();
    std::cout << version.description << ": " << version.buildNumber << std::endl;

    // -------- Step 1. Initialize OpenVINO Runtime Core --------
    ov::Core core;

    // -------- Step 2. Compile the Model --------
    auto compiled_model = core.compile_model("yolov8s-cls.xml", "CPU");

    // -------- Step 3. Create an Inference Request --------
    ov::InferRequest infer_request = compiled_model.create_infer_request();

    // -------- Step 4.Read a picture file and do the preprocess --------
    cv::Mat img = cv::imread("image.jpg"); 
    // Preprocess the image
    int col = img.cols;
    int row = img.rows;
    int _max = MAX(col, row);
    cv::Mat letterbox_img = cv::Mat::zeros(_max, _max, CV_8UC3);
    img.copyTo(letterbox_img(cv::Rect(0, 0, col, row)));
    
    cv::Mat blob = cv::dnn::blobFromImage(letterbox_img, 1.0 / 255.0, cv::Size(224, 224), cv::Scalar(), true);

    // -------- Step 5. Feed the blob into the input node of the Model -------
    // Get input port for model with one input
    auto input_port = compiled_model.input();
    std::cout << "The shape of input tensor:" << input_port.get_shape() << std::endl;
    // Create tensor from external memory
    ov::Tensor input_tensor(input_port.get_element_type(), input_port.get_shape(), blob.ptr(0));
    // Set input tensor for model with one input
    infer_request.set_input_tensor(input_tensor);

    // -------- Step 6. Start inference --------
    infer_request.infer();
    struct timeval start_time, end_time;
    gettimeofday(&start_time,NULL);
    infer_request.infer();
    gettimeofday(&end_time,NULL);
    // Get the elapsed millisecond time
    double elapsed_time = (end_time.tv_sec - start_time.tv_sec)*1000 + (end_time.tv_usec - start_time.tv_usec)/1000;
    // -------- Step 7. Get the inference result --------
    auto output = infer_request.get_output_tensor(0);
    auto output_shape = output.get_shape();
    std::cout << "The shape of output tensor:" << output_shape << std::endl;

    // -------- Step 8. Postprocess the result --------
    float* output_buffer = output.data<float>();
    std::vector<float> result(output_buffer, output_buffer + output_shape[1]);
    auto max_idx = std::max_element(result.begin(), result.end());
    int class_id = max_idx - result.begin();
    float score = *max_idx;
    std::cout << "Class ID:" << class_id << " Score:" <<score<< std::endl;
    std::cout << "infer time:" <<elapsed_time<< std::endl;
    return 0;
}

在该代码中,主要是获取OpenVINO™版本信息,然后按照模型部署流程部署测试了Yolov8图片分类模型,并打印输出结果以及推理时间。

4. 项目编译运行

在该项目中通过CMake编译项目,定义的
CMakeLists.txt
文件如下所示:

cmake_minimum_required(VERSION 3.28)
project(test_openvino)
set(OpenCV_DIR /Users/ygj/3lib/opencv_4.8.0/lib/cmake/opencv4)
find_package(OpenCV REQUIRED)
message(STATUS "OpenCV_DIR = ${OpenCV_DIR}")
message(STATUS "OpenCV_INCLUDE_DIRS = ${OpenCV_INCLUDE_DIRS}")
message(STATUS "OpenCV_LIBS = ${OpenCV_LIBS}")
set(OpenVINO_DIR /Users/ygj/3lib/openvino_2023.2/runtime/cmake)
set(OpenVINO_LIBs "/Users/ygj/3lib/openvino_2023.2/runtime/lib/arm64/Release/libopenvino.2320.dylib")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
include_directories(
    /Users/ygj/3lib/openvino_2023.2/runtime/include
    ${OpenCV_INCLUDE_DIRS}
)
add_executable(test_openvino test_openvino.cpp )
target_link_libraries(test_openvino ${OpenVINO_LIBs} ${OpenCV_LIBS})

在这个CMakeLists文件中,需要同时配置
OpenCV
以及
OpenVINO
这两个依赖库,具体编译以及配置方式参考CMake手册。

接下来就可以项目编译了,在终端中输入一下命令,就可以进行项目配置了,输出结果如下所示:

cmake .

image

接下来就是进行项目编译,CMake编译后会生成
Makefile
文件,之后就可以运行
make
命令进行项目最后的编译,然后就可以直接运行生成的项目文件,如下所示:

make
./test_openvino

image

上图中展示了项目最后运行结果,可以看出,此处使用的模型输入大小为[1,3,224,224],输出大小为[1,1000],识别结果
Class ID=386
,查看分类结果字典,图片识别结果与图片一致;模型的推理时间为:7ms。

5. 总结

该项目中,我们在MacOS 14.2.1 系统、M2芯片的 Macbook Air 电脑上,成功使用OpenVINO™ C++ API 部署了Yolov8图片分类深度学习模型,并详细演示了OpenVINO™ C++ API在苹果电脑上使用与配置流程,为使用MacOS系统的开发者提供了很好的范例与参考。