【速写】KV-cache与解码的再探讨(以束搜索实现为例)

文章目录

  • 1 Beam Search 解码算法实现
  • 2 实现带KV Cache的Beam Search解码
  • 3 关于在带kv-cache的情况下的use_cache参数


1 Beam Search 解码算法实现

下面是一个使用PyTorch实现的beam search解码算法:

几个小细节:

  • 束搜索可以加入length_penalty,目前model.generate也是有这个参数的,这个惩罚项直接是用来除生成概率的
  • 通常这种需要计算概率相乘的情况,都是避免做乘法,而是使用log p相加
  • 具体实现中应当考虑eos标识符导致的early stop的候选序列,需要提前存储到外面
  • 然后就是关于使用log softmax得到log概率后,这其实是一个负的概率,序列越长,log prob会越小,- log prob 才是越大的,因此在做惩罚的时候,应该是吧 prob / len(seq) ** penality,即长序列的 log prob 会被除掉更多,这是合理的,因为短序列的 - log prob 天然地比 长序列地 - log prob 要更小,这样量纲才是正确的
import torch
import torch.nn.functional as F
from typing import List, Tupledef beam_search(model: torch.nn.Module,initial_input: torch.Tensor,beam_width: int,max_length: int,vocab_size: int,device: torch.device,length_penalty: float = 1.0,early_stopping: bool = True
) -> Tuple[List[List[int]], List[float]]:"""Beam search 解码算法实现参数:model: 用于预测下一个token的模型initial_input: 初始输入张量 (shape: [1, seq_len])beam_width: beam大小max_length: 生成序列的最大长度vocab_size: 词汇表大小device: 使用的设备 (cpu/cuda)length_penalty: 长度惩罚系数 (α), 用于调整对长序列的偏好early_stopping: 是否在所有beam序列达到EOS时提前停止返回:Tuple[List[List[int]], List[float]]: (生成的序列列表, 对应的分数列表)"""# 初始化beamsequences = [[initial_input.tolist()[0]]]  # 初始序列scores = [0.0]  # 初始分数 (log概率)# 存储完整的beam (已经生成EOS的序列)completed_sequences = []completed_scores = []for step in range(max_length):# 如果所有beam都已完成,提前停止if early_stopping and len(sequences) == 0:break# 准备当前步的输入candidates = []for i, seq in enumerate(sequences):# 跳过已经完成的序列if len(seq) > 0 and seq[-1] == 2:  # 假设2是EOS tokencompleted_sequences.append(seq)completed_scores.append(scores[i])continue# 将序列转换为张量input_tensor = torch.tensor([seq], dtype=torch.long).to(device)# 获取模型预测with torch.no_grad():outputs = model(input_tensor)next_token_logits = outputs[:, -1, :]  # 取最后一个token的logitsnext_token_probs = F.log_softmax(next_token_logits, dim=-1)# 获取top-k tokens和它们的log概率topk_probs, topk_tokens = torch.topk(next_token_probs, beam_width, dim=-1)topk_probs = topk_probs.squeeze(0)topk_tokens = topk_tokens.squeeze(0)# 创建候选序列for j in range(beam_width):new_seq = seq.copy()new_seq.append(topk_tokens[j].item())new_score = scores[i] + topk_probs[j].item()candidates.append((new_seq, new_score))# 如果没有候选序列,提前停止if not candidates:break# 选择top-k候选序列,并剪枝candidates.sort(key=lambda x: x[1] / (len(x[0]) ** length_penalty), reverse=True)sequences, scores = zip(*candidates[:beam_width])sequences = list(sequences)scores = list(scores)# 添加剩余的未完成序列到完成列表中completed_sequences.extend(sequences)completed_scores.extend(scores)# 对完成的序列按分数排序sorted_sequences = [seq for seq, _ in sorted(zip(completed_sequences, completed_scores),key=lambda x: x[1] / (len(x[0]) ** length_penalty),reverse=True)]sorted_scores = sorted(completed_scores,key=lambda score: score / (len(sorted_sequences[completed_scores.index(score)]) ** length_penalty),reverse=True)return sorted_sequences, sorted_scores
  1. 模型要求:

    • 模型应接受形状为 [batch_size, seq_len] 的输入
    • 模型应输出形状为 [batch_size, seq_len, vocab_size] 的logits
  2. 参数说明:

    • initial_input: 初始输入序列(如开始token)
    • beam_width: 控制搜索宽度,值越大结果可能越好但计算成本越高
    • length_penalty: 控制生成长度的偏好(α>1鼓励更长序列,α<1鼓励更短序列)
    • early_stopping: 设为True时,当所有beam都生成EOS token时停止
  3. 返回值:

    • 返回按分数排序的序列列表和对应的分数列表

如何调用?

model = ...  # 你的PyTorch模型
initial_input = torch.tensor([[1]])  # 假设1是开始token
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)sequences, scores = beam_search(model=model,initial_input=initial_input,beam_width=5,max_length=50,vocab_size=10000,  # 你的词汇表大小device=device,length_penalty=1.2
)print("Top sequence:", sequences[0])
print("Score:", scores[0])

2 实现带KV Cache的Beam Search解码

这个是关键,之前写过一个k步的贪心解码,使用了递归写法,但不知道怎么在递归中加入kv cache,其实感觉怎么写都不太好。

其实这样看只要写循环就可以了。

实际上transformers的model.generate方法里,对greedy、beam、以及其他采样的方法都是有做 kv cache 缓存优化的:

# 首次调用(处理初始输入)
outputs = model(input_ids, use_cache=True)
logits = outputs.logits
past_key_values = outputs.past_key_values  # 缓存KV# 后续步骤(生成时)
next_token_input = torch.tensor([[new_token]])
outputs = model(next_token_input, past_key_values=past_key_values, use_cache=True)
new_past_key_values = outputs.past_key_values  # 更新缓存

要在Beam Search中为每个候选序列维护独立的KV Cache,我们需要对标准实现进行一些修改。KV Cache可以显著提高自回归模型的推理效率,因为它避免了重复计算先前token的key和value。

以下是带有KV Cache的Beam Search实现:

  • 因为我们已经有cache了,因此实际上每步迭代生成下一个token时,只需要把当前序列的最后一个token输入即可,只是很棒的操作
import torch
import torch.nn.functional as F
from typing import List, Tuple, Optional, Dict, Anydef beam_search_with_kv_cache(model: torch.nn.Module,initial_input: torch.Tensor,beam_width: int,max_length: int,vocab_size: int,device: torch.device,length_penalty: float = 1.0,early_stopping: bool = True,use_kv_cache: bool = True
) -> Tuple[List[List[int]], List[float]]:"""带KV Cache的Beam Search解码算法参数:model: 用于预测下一个token的模型initial_input: 初始输入张量 (shape: [1, seq_len])beam_width: beam大小max_length: 生成序列的最大长度vocab_size: 词汇表大小device: 使用的设备 (cpu/cuda)length_penalty: 长度惩罚系数early_stopping: 是否在所有beam序列达到EOS时提前停止use_kv_cache: 是否使用KV Cache加速返回:Tuple[List[List[int]], List[float]]: (生成的序列列表, 对应的分数列表)"""# 初始化beamsequences = [[initial_input.tolist()[0]]]scores = [0.0]# 存储KV Cache (每个候选序列一个cache)kv_caches = [None]  # 初始cache为None# 存储完整的beamcompleted_sequences = []completed_scores = []for step in range(max_length):if early_stopping and len(sequences) == 0:breakcandidates = []new_kv_caches = []for i, (seq, score, kv_cache) in enumerate(zip(sequences, scores, kv_caches)):# 跳过已经完成的序列if len(seq) > 0 and seq[-1] == 2:  # 假设2是EOS tokencompleted_sequences.append(seq)completed_scores.append(score)continue# 准备输入 (只使用最后一个token,因为前面的已经cache了)input_tensor = torch.tensor([[seq[-1]]], dtype=torch.long).to(device)# 前向传播,使用或更新KV Cachewith torch.no_grad():if use_kv_cache:if kv_cache is None:# 第一次调用,处理整个初始序列full_input = torch.tensor([seq], dtype=torch.long).to(device)outputs = model(full_input, use_cache=True)next_token_logits = outputs.logits[:, -1, :]new_kv_cache = outputs.past_key_valueselse:# 后续调用,使用KV Cacheoutputs = model(input_tensor, past_key_values=kv_cache, use_cache=True)next_token_logits = outputs.logits[:, -1, :]new_kv_cache = outputs.past_key_valueselse:# 不使用KV Cache的情况full_input = torch.tensor([seq], dtype=torch.long).to(device)outputs = model(full_input, use_cache=False)next_token_logits = outputs.logits[:, -1, :]new_kv_cache = Nonenext_token_probs = F.log_softmax(next_token_logits, dim=-1)# 获取top-k tokenstopk_probs, topk_tokens = torch.topk(next_token_probs, beam_width, dim=-1)topk_probs = topk_probs.squeeze(0)topk_tokens = topk_tokens.squeeze(0)# 创建候选序列for j in range(beam_width):new_seq = seq.copy()new_seq.append(topk_tokens[j].item())new_score = score + topk_probs[j].item()candidates.append((new_seq, new_score, new_kv_cache))# 如果没有候选序列,提前停止if not candidates:break# 选择top-k候选序列candidates.sort(key=lambda x: x[1] / (len(x[0]) ** length_penalty), reverse=True)top_candidates = candidates[:beam_width]# 解包候选序列sequences = [cand[0] for cand in top_candidates]scores = [cand[1] for cand in top_candidates]kv_caches = [cand[2] for cand in top_candidates]# 添加剩余的未完成序列completed_sequences.extend(sequences)completed_scores.extend(scores)# 对完成的序列按分数排序sorted_pairs = sorted(zip(completed_sequences, completed_scores),key=lambda x: x[1] / (len(x[0]) ** length_penalty),reverse=True)sorted_sequences = [pair[0] for pair in sorted_pairs]sorted_scores = [pair[1] for pair in sorted_pairs]return sorted_sequences, sorted_scores

关键修改说明

  1. KV Cache维护:

    • 每个候选序列现在维护自己的KV Cache
    • 初始时KV Cache为None
    • 第一次处理序列时生成完整的KV Cache
    • 后续步骤只处理最后一个token并更新KV Cache
  2. 模型接口要求:

    • 模型需要支持past_key_values参数来接收先前的KV Cache
    • 模型需要返回past_key_values作为输出的一部分
    • 典型实现方式(如HuggingFace的transformers):
      outputs = model(input_ids, past_key_values=past_key_values, use_cache=True)
      next_token_logits = outputs.logits
      past_key_values = outputs.past_key_values
      
  3. 性能优化:

    • 使用KV Cache后,每次前向传播只处理最后一个token(这个很有趣,但是要设置use_cache=True
    • 避免了重复计算先前token的key和value
    • 对于长序列可以显著提高速度

一个简单的调用示例:

# 假设我们有一个支持KV Cache的模型
model = ...  # 例如HuggingFace的GPT2模型
initial_input = torch.tensor([[model.config.bos_token_id]])  # 开始token
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)# 运行带KV Cache的beam search
sequences, scores = beam_search_with_kv_cache(model=model,initial_input=initial_input,beam_width=5,max_length=50,vocab_size=model.config.vocab_size,device=device,length_penalty=1.2,use_kv_cache=True  # 启用KV Cache
)print("Top sequence:", sequences[0])
print("Score:", scores[0])

补注:

在这个部分:

            # 前向传播,使用或更新KV Cachewith torch.no_grad():if use_kv_cache:if kv_cache is None:# 第一次调用,处理整个初始序列full_input = torch.tensor([seq], dtype=torch.long).to(device)outputs = model(full_input, use_cache=True)

上,输出的full_input 的size是[1, 1, seqlen],理论上应该是[1, seqlen]才对,因此要么是

            # 前向传播,使用或更新KV Cachewith torch.no_grad():if use_kv_cache:if kv_cache is None:# 第一次调用,处理整个初始序列full_input = torch.tensor(seq, dtype=torch.long).to(device)outputs = model(full_input, use_cache=True)

要么是:

            # 前向传播,使用或更新KV Cachewith torch.no_grad():if use_kv_cache:if kv_cache is None:# 第一次调用,处理整个初始序列full_input = torch.tensor([seq], dtype=torch.long).to(device)outputs = model(full_input.squeeze(0), use_cache=True)

这样测试跑通应该是没有问题的


3 关于在带kv-cache的情况下的use_cache参数

比如之前手写的一个贪心解码算法:

# -*- coding: utf8 -*-
# @author: caoyang
# @email: caoyang@stu.sufe.edu.cnimport torch
import logging
from copy import deepcopy
from functools import wraps
from torch.nn import functional as Ffrom transformers import AutoTokenizer, AutoModelForCausalLM# Standard greedy decode
# @param model: Huggingface model object
# @param tokenizer: Huggingface tokenizer Object
# @param prompt: Str
# @param max_length: Int, the number of tokens to be generated
# @param device: Str, e.g. "cuda" or "cpu"
# @param kv_cache: Boolean, whether to use KV-cache to accelerate, if True then large memory will be consumed
# @return generated_text: Str
# @return generated_token_prob: List[Tuple(Int, Str, Float)], `len(generated_id_prob)` is `max_length`, indicating the generated probability of each token
# @return generated_logits: Tuple[FloatTensor(1, n_vocab)], `len(generated_logits)` is `max_length`, indicating the logits when each token is generated
def greedy_decode(model,tokenizer,prompt, max_length,device = "cuda",kv_cache = True,):inputs = tokenizer.encode(prompt, return_tensors="pt").to(device)	# Str => Long(1, n_tokens)past_key_values = Nonegenerated_token_probs = list()generated_logits = list()model.gradient_checkpointing_enable()for i in range(max_length):logging.info(f"Round {i}: {past_key_values.key_cache[0].size() if past_key_values is not None else None}")outputs = model(inputs, past_key_values=past_key_values)logits = outputs.logits	# Float(1, n_tokens + i + 1, n_vocab), where `n_vocab` is 151936 in DeepSeek-R1-Distill-Qwenif kv_cache:past_key_values = outputs.past_key_values	# Dictlike[key_cache: Float(1, 2, X, hidden_size), value_cache: Float(1, 2, X, hidden_size)], where X = (i + 1) * (n_tokens + i / 2)next_token_probs = F.softmax(logits[:, -1, :], dim=-1)	# Float(1, n_tokens + i + 1, n_vocab) => Float(1, n_vocab)next_token_id = torch.argmax(next_token_probs, dim=-1)	# Float(1, n_vocab) => Long(1, )next_token_prob = next_token_probs[0, next_token_id].item()	# Float(1, n_vocab) => Float()next_token = tokenizer.decode(next_token_id[0].item(), skip_special_tokens=False)	# Long(1, ) => Strinputs = torch.cat([inputs, next_token_id.unsqueeze(-1)], dim=-1)	# Long(1, n_tokens + i) => Long(1, n_tokens + i + 1)generated_token_probs.append((next_token_id.item(), next_token, next_token_prob))generated_logits.append(logits[:, -1, :])generated_text = tokenizer.decode(token_ids = inputs[0], skip_special_tokens=True, clean_up_tokenization_spaces=True,)	# Long(1, n_tokens + max_length) => Strreturn generated_text, generated_token_probs, tuple(generated_logits)

实际上除了第一次输入外,接下来都可以用最后一个token作为输入,而不需要把之前整个一长串的input都输入到model中去:

# -*- coding: utf8 -*-
# @author: caoyang
# @email: caoyang@stu.sufe.edu.cnimport torch
import logging
from copy import deepcopy
from functools import wraps
from torch.nn import functional as Ffrom transformers import AutoTokenizer, AutoModelForCausalLM# Standard greedy decode
# @param model: Huggingface model object
# @param tokenizer: Huggingface tokenizer Object
# @param prompt: Str
# @param max_length: Int, the number of tokens to be generated
# @param device: Str, e.g. "cuda" or "cpu"
# @param kv_cache: Boolean, whether to use KV-cache to accelerate, if True then large memory will be consumed
# @return generated_text: Str
# @return generated_token_prob: List[Tuple(Int, Str, Float)], `len(generated_id_prob)` is `max_length`, indicating the generated probability of each token
# @return generated_logits: Tuple[FloatTensor(1, n_vocab)], `len(generated_logits)` is `max_length`, indicating the logits when each token is generated
def greedy_decode(model,tokenizer,prompt, max_length,device = "cuda",kv_cache = True,):inputs = tokenizer.encode(prompt, return_tensors="pt").to(device)	# Str => Long(1, n_tokens)past_key_values = Nonegenerated_token_probs = list()generated_logits = list()model.gradient_checkpointing_enable()for i in range(max_length):logging.info(f"Round {i}: {past_key_values.key_cache[0].size() if past_key_values is not None else None}")if kv_cache:if i == 0:outputs = model(inputs, past_key_values=past_key_values)else:outputs = model(inputs[:, -1].unsqueeze(0), past_key_values=past_key_values, use_cache=True)else:outputs = model(inputs, past_key_values=None)logits = outputs.logits	# Float(1, n_tokens + i + 1, n_vocab), where `n_vocab` is 151936 in DeepSeek-R1-Distill-Qwenif kv_cache:past_key_values = outputs.past_key_values	# Dictlike[key_cache: Float(1, 2, X, hidden_size), value_cache: Float(1, 2, X, hidden_size)], where X = (i + 1) * (n_tokens + i / 2)next_token_probs = F.softmax(logits[:, -1, :], dim=-1)	# Float(1, n_tokens + i + 1, n_vocab) => Float(1, n_vocab)next_token_id = torch.argmax(next_token_probs, dim=-1)	# Float(1, n_vocab) => Long(1, )next_token_prob = next_token_probs[0, next_token_id].item()	# Float(1, n_vocab) => Float()next_token = tokenizer.decode(next_token_id[0].item(), skip_special_tokens=False)	# Long(1, ) => Strinputs = torch.cat([inputs, next_token_id.unsqueeze(-1)], dim=-1)	# Long(1, n_tokens + i) => Long(1, n_tokens + i + 1)generated_token_probs.append((next_token_id.item(), next_token, next_token_prob))generated_logits.append(logits[:, -1, :])generated_text = tokenizer.decode(token_ids = inputs[0], skip_special_tokens=True, clean_up_tokenization_spaces=True,)	# Long(1, n_tokens + max_length) => Strreturn generated_text, generated_token_probs, tuple(generated_logits)

这个确实是很有帮助的,能加速推理很多。这个原理其实很简单,因为只需要KVcache与最后一个token就可以计算得到下一层的注意力权重(其实就是下一轮生成的KVcache),然后倒是发现deepseek在生成图像链接时出错了,难得逮到DeepSeek犯错的时候:

在这里插入图片描述

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

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

相关文章

ABP-Book Store Application中文讲解 - 前期准备 - Part 3:Acme.BookStore项目模块详解之二

1. 汇总 ABP-Book Store Application中文讲解-汇总-CSDN博客 2. 前一章 ABP-Book Store Application中文讲解 - 前期准备 - Part 3:Acme.BookStore项目模块详解 项目之间的引用关系。 目录 1. .Domain.Shared 2. .Domain 3. .Application.Contracts 4. .Application 5…

【Leetcode刷题随笔】349. 两个数组的交集

1. 题目描述 给定两个数组nums1和nums2&#xff0c;返回它们的交集。输出结果中的每个元素一定是唯一的。我们可以不考虑输出结果的顺序。 示例1: 输入:nums1 [1,2,2,1], nums2 [2,2] 输出&#xff1a;[2] 题目条件&#xff1a; 1 < nums1.length, nums2.length < 10…

Unity打包安卓失败 Build failure 解决方法

【Unity】打包安卓失败 Build failure 的解决方法_com.android.build.gradle.internal.res.linkapplicat-CSDN博客 unity在打包时设置手机屏幕横屏竖屏的方法_unity打包默认横屏-CSDN博客

Window、CentOs、Ubuntu 安装 docker

Window 版本 网址&#xff1a;https://www.docker.com/ 下载 下载完成后&#xff0c;双击安装就可以了 Centos 版本 卸载 Docker &#xff08;可选&#xff09; yum remove docker \docker-client \docker-client-latest \docker-common \docker-latest \docker-latest-log…

Matlab自学笔记五十四:符号数学工具箱和符号运算、符号求解、绘图

1.什么是符号数学工具箱&#xff1f; 符号数学工具箱是Matlab针对符号对象的运算功能&#xff0c;它引入了一种特殊的数据类型 - 符号对象&#xff1b; 该数据类型包括符号数字&#xff0c;符号变量&#xff0c;符号表达式和符号函数&#xff0c;还包含符号矩阵&#xff0c;以…

OpenCV进阶操作:图像的透视变换

文章目录 前言一、什么是透视变换&#xff1f;二、透视变换的过程三、OpenCV透视变换核心函数四、文档扫描校正&#xff08;代码&#xff09;1、预处理2、定义轮廓点的排序函数3、定义透视变换函数4、读取原图并缩放5、轮廓检测6、绘制最大轮廓7、对最大轮廓进行透视变换8、旋转…

【python】基础知识点100问

以下是Python基础语法知识的30条要点整理,涵盖数据类型、函数、控制结构等核心内容,结合最新资料归纳总结: 基础30问 一、函数特性 函数多返回值 支持用逗号分隔返回多个值,自动打包为元组,接收时可解包到多个变量 def func(): return 1, "a" x, y = func()匿…

采用AI神经网络降噪算法的通信语音降噪(ENC)模组性能测试和应用

采用AI降噪的语言通话环境抑制模组性能效果测试 随着AI时代来临.通话设备的环境噪音抑制也进入AI降噪算法时代. AI神经网络降噪技术是一款革命性的语音处理技术&#xff0c;他突破了传统单麦克风和双麦克风降噪的局限性,利用采集的各种日常环境中的噪音样本进行训练学习.让降噪…

openwrt目录结构(部分)

1&#xff0c;openwrt 原始目录需要注意的目录 tools: 该目录下存放着一些&#xff0c;编译工程的自动化工具包和一些在编译过程用到的命令包&#xff0c; 查看目录下的Makefile&#xff0c;知道其会在编译过程中将依赖包下载 例如&#xff1a; autoconf / lzma / mkimage/ …

RDB和AOF的区别

Redis提供两种主要的持久化机制&#xff1a;RDB&#xff08;Redis Database&#xff09;和AOF&#xff08;Append Only File&#xff09;&#xff0c;它们在数据持久化方式、性能影响及恢复策略上各有特点。以下是两者的对比分析及使用建议&#xff1a; RDB&#xff08;快照持久…

基于大模型的甲状腺结节诊疗全流程预测与方案研究报告

目录 一、引言 1.1 研究背景与目的 1.2 研究意义 1.3 国内外研究现状 二、大模型预测原理与方法 2.1 相关大模型概述 2.2 数据收集与预处理 2.3 模型训练与验证 三、术前预测与评估 3.1 结节性质预测 3.1.1 良恶性判断 3.1.2 与传统诊断方法对比 3.2 手术风险预测…

逆向破解:x64dbg

文章目录 一、CPU窗口1、反汇编窗口2、寄存器窗口3、栈地址窗口4、十六进制数据窗口5、堆栈参数解析窗口 二、常用快捷键三、字符串检索功能四、调试功能1、上一步 一、CPU窗口 1、反汇编窗口 2、寄存器窗口 寄存器窗口用于显示和解释当前线程环境下CPU寄存器的各种状态值和内…

免布线视频桩如何重塑停车管理模式

传统停车管理常因布线复杂、维护成本高而难以推广&#xff0c;而“免布线视频桩”通过无线设计、低功耗与高精度检测&#xff0c;为城市停车提供高效解决方案。作为智慧城市建设的创新工具&#xff0c;免布线视频桩以即装即用、长效续航等特性&#xff0c;正在重塑停车管理模式…

【CTFer成长之路】举足轻重的信息搜集

举足轻重的信息搜集 信息搜集 常见的搜集 题目描述: 一共3部分flag docker-compose.yml version: 3.2services:web:image: registry.cn-hangzhou.aliyuncs.com/n1book/web-information-backk:latestports:- 80:80启动方式 docker-compose up -d 题目Flag n1book{info_…

springboot3+vue3融合项目实战-大事件文章管理系统-更新用户密码

大致分为这三步 首先在usercontroller中增加updatePwd方法 PatchMapping ("/updatePwd")public Result updatePwd(RequestBody Map<String,String> params){//1.校验参数String oldPwd params.get("old_pwd");String newPwd params.get("n…

OpenCV进阶操作:指纹验证、识别

文章目录 前言一、指纹验证1、什么是指纹验证2、流程步骤 二、使用步骤&#xff08;案例&#xff09;三、指纹识别&#xff08;案例&#xff09;1、这是我们要识别的指纹库2、这是待识别的指纹图3、代码4、结果 总结 前言 指纹识别作为生物识别领域的核心技术之一&#xff0c;…

ECLIC中断流程及实际应用 —— RISC-V中断机制(二)

在长期的嵌入式开发实践中&#xff0c;对中断机制的理解始终停留在表面层次&#xff0c;特别当开发者长期局限于纯软件抽象层面时&#xff0c;对中断机制的理解极易陷入"知其然而不知其所以然"的困境&#xff0c;这种认知的局限更为明显&#xff1b;随着工作需要不断…

计算机网络-LDP标签发布与管理

前面学习了LDP建立邻居&#xff0c;建立会话&#xff0c;今天来学习在MPLS中的标签发布与管理。 在MPLS网络中&#xff0c;下游LSR决定标签和FEC的绑定关系&#xff0c;并将这种绑定关系发布给上游LSR。LDP通过发送标签请求和标签映射消息&#xff0c;在LDP对等体之间通告FEC和…

Go语言运算符详解

文章目录 1. 算术运算符2. 关系运算符3. 逻辑运算符4. 位运算符5. 赋值运算符6. 其他运算符运算符优先级注意事项 Go语言提供了与其他语言类似的运算符&#xff0c;包括算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符等。这些运算符即可满足基本的运算需求。 1. 算…

Selenium模拟人类行为,操作网页的方法(全)

看到有朋友评论问&#xff0c;用selenium怎么模仿人类行为&#xff0c;去操作网页的页面呢&#xff1f; 我想了想&#xff0c;这确实是一个很大的点&#xff0c;不应该是一段代码能解决的&#xff0c; 就像是,如果让程序模拟人类的行为。例如模拟人类买菜&#xff0c;做饭&am…