LLM | Tokenization 从原理与代码了解GPT的分词器

声明:以上内容全是学习Andrej Karpathy油管教学视频的总结。

---------------------------------------------------------------------------------------------------------------------------------

大家好。在今天我们学习llm中的Tokenization,即分词器部分。许多人可能之前对于这个过程没有太多的重视。但是实际上,LLM中许多奇怪的问题都可以追溯到Tokenization的过程中

  • 无法拼写单词
  • 无法倒写单词
  • 处理"<|endoftext|>"之类特殊字符时易出现混乱
  • 为什么LLM相比于JSON而言,面对YAML文件更加友好。
  • ...

这一切的根源都是在于模型的Tokenization部分。下面我为大家进行一一讲解。在讲解之前,我需要首先给大家简单的讲解一下,什么是模型的Tokenization?

原理

这里我引入State of GPT文章中的一些插图。文章里详细的解释了ChatGPT等LLM的技术路线,建议大家去看一下原视频。

如上图所示,在LLM的基础模型训练过程中,模型的输入是 (B, T) 的Token序列。而模型所得到的输出也即是预测下一个Token出现的概率分布。而这些数字,则是通过Tokenizer得到的。其生成流程如下。

这里我们看见,我们把文本按照一种奇怪的方式分割成了一个又一个的 sub word。目前大家只需要理解 Tokens部分一个颜色的块则对应模型训练的一个Token,而对于每一个Token,模型内部会有一个字典 vocab 对应一个 int 整数。这就是Tokenazition的过程。简而言之,Tokenization可以理解为把一串字符串转换成整数的列表

现在我们理解了Tokenization的基本原理,我们对于上面的四个问题先给予一下简单的答复。

1. 无法拼写单词?

这里我们以一个单词 .DefaultCellStyle 为例子。在GPT4的分词器中,将这一长串文本分为了一个Token 98518。因此单词里的所有信息被压缩在了一个Token中。

因此若你询问GPT4有多少个l,会得到怎样的结果?如图...

2. 无法倒写单词

同理,如果我问如果反过来拼写 .DefaultCellStyle, 他会回答我一个奇怪的答案。

3. 处理"<|endoftext|>"之类特殊字符时易出现混乱

这是因为这些特殊字符有时在模型中具有其意义。因此某些时候存在问题。在Karpathy的视频里是存在问题的,不过目前我现在GPT是4o,貌似效果还好。

4.为什么LLM相比于JSON而言,面对YAML文件更加友好

这是因为相同内容的JSON和YAML文件,YAML文件的Token数更少,这是相当大的改进。Token少可以减少上下文长度。

代码

训练流程

在这里我们需要了解一个核心数据压缩算法BPE (字节对编码, Byte Pair Encoder) 。 简单的文字叙述可能不好理解,这里我直接以代码案例帮助大家理解。

text = "aaabdaaabac"
ids = list(text.encode(encoding="utf-8", errors="replace"))
print(ids)
# [97, 97, 97, 98, 100, 97, 97, 97, 98, 97, 99]

这里我们list的作用是把utf-8的字节流转换成int,并且处理成int的形式。那么接下来我们就要统计这一个ids列表里面出现的字节对的次数。

def get_stats(ids: list, count=None) -> dict:"""找到字节对的统计次数Example: [1, 2, 1, 2, 3] -> {(1, 2): 2, (2, 1): 1, (2, 3): 1}:params ids: list, 字节流:return count: dict, 字节对的统计次数"""count = {} if count is None else countfor (p0, p1) in zip(ids, ids[1:]):count[(p0, p1)] = count.get((p0, p1), 0) + 1return countcounts = get_stats(ids)
print(counts)   
# {(97, 97): 4, (97, 98): 2, (98, 100): 1, (100, 97): 1, (98, 97): 1, (97, 99): 1}

这里我们就知道97,97是出现了4次的。那么接下来,我们就要把(97, 97)这个字节对用一个新的id来替代。

def merge(ids: list, pair: tuple, idx: int) -> list:"""将字节对用最新idx替换Example: ids=[1, 2, 3, 1, 2], pair=(1, 2), idx = 4 -> [4, 3, 4]:params ids: list, 原字节串:params pair: tuple, 原字节对:params idx: int, 新索引:return new_ids: list, 新字节串"""new_ids = []i = 0while i < len(ids):if i < len(ids) - 1 and ids[i] == pair[0] and ids[i + 1] == pair[1]:new_ids.append(idx)i = i + 2else:new_ids.append(ids[i])i = i + 1return new_idsnew_id = merge(ids, (97, 97), 256)
print(new_id)
# [256, 97, 98, 100, 256, 97, 98, 97, 99]

以上的流程就完成了一次字节对的替换。同时我们的词汇也增加了一。这里大家可能疑惑词汇是什么。这里需要解释一下,由于utf-8编码是字节的变长排序。所以在训练的过程中,我们一般会把0-255的字节默认存储在字典中。同时替换一次,我们的词汇就会增加一。替换次数越多,词汇量越多,压缩的就越多,训练过程中能承载的原始文本原理上可以更多。

但是,词汇量并不是越多越好,了解Transformer结构的同学应该能理解,词汇量增多的话,会出现几个问题:

  • 出现许多低频词汇,学习变得困难
  • 参数量增多,增加计算成本
  • 过拟合
  • ...

因此,这个词汇量也是在模型训练过程中需要权衡的一个点。要既能捕捉复杂语言的细微差别,也要权衡上面的因素

上面的流程只是一次字节对替换的流程,接下来我把完整的训练流程以及中间变量尽可能详细的给大家通过代码展示出来。

vocab_size = 256 + 3   # 词汇量大小
num_merges = vocab_size - 256   # merge次数idx = 256
vocab = {i: bytes([i]) for i in range(256)} # 初始的词汇
merges = {} # (int, int) -> inttext = "aaabdaaabac"
ids = list(text.encode(encoding="utf-8", errors="replace"))# 进行字节对的替换
for i in range(num_merges):new_id = idx + i # 字节对的新编号stats = get_stats(ids)pair = max(stats, key=stats.get)    # 出现次数最多的字节对ids = merge(ids, pair, new_id)  # 替换字节对vocab[new_id] = vocab[pair[0]] + vocab[pair[1]]merges[pair] = new_idprint(f"{pair} -> {new_id} {vocab[new_id].decode("utf-8")}")# (97, 97) -> 256 aa
# (256, 97) -> 257 aaa
# (257, 98) -> 258 aaab

接下来,我们还需要编写两个重要的函数 encoder 与 decoder。作用当然大家也清楚:完成文本与ids之间的转换。

def decode(ids):tokens = b"".join(vocab[idx] for idx in ids)text = tokens.decode("utf-8", errors="replace")return text# 这里要注意BPE的合并顺序
def encode(text):tokens = list(text.encode("utf-8"))while len(tokens) >= 2:stats = get_stats(tokens)pair = min(stats, key=lambda p: merges.get(p, float("inf")))if pair not in merges:breaktokens = merge(tokens, pair, merges[pair])return tokens

大家可以自己尝试一下。

以上即为一次完整的训练过程。当然这相比于GPT的部分还是缺少了一些东西。不过不要紧,在下面我会以下面的内容为基础,为大家构建简单的Tokenizer。

Base

这里我们在文件夹下的base.py中创建一个用于继承的基础类,完成一些基本函数,制定标准。

import unicodedatadef get_stats(ids: list, count=None) -> dict:"""找到字节对的统计次数Example: [1, 2, 1, 2, 3] -> {(1, 2): 2, (2, 1): 1, (2, 3): 1}:params ids: list, 字节流:return count: dict, 字节对的统计次数"""count = {} if count is None else countfor (p0, p1) in zip(ids, ids[1:]):count[(p0, p1)] = count.get((p0, p1), 0) + 1return countdef merge(ids: list, pair: tuple, idx: int) -> list:"""将字节对用最新idx替换Example: ids=[1, 2, 3, 1, 2], pair=(1, 2), idx = 4 -> [4, 3, 4]:params ids: list, 原字节串:params pair: tuple, 原字节对:params idx: int, 新索引:return new_ids: list, 新字节串"""new_ids = []i = 0while i < len(ids):if i < len(ids) - 1 and ids[i] == pair[0] and ids[i + 1] == pair[1]:new_ids.append(idx)i = i + 2else:new_ids.append(ids[i])i = i + 1return new_idsdef replace_control_charactors(s: str) -> str:"""去除字符串中的控制字符, 如"\n" 用unicode码表示Example: "hello \n world" -> "hello \u000a world":params s: str, 原字符串:return : str, 新字符串"""chars = []for ch in s:if unicodedata.category(ch)[0] == "C":chars.append(f"\\u{ord(ch):04x}")else:chars.append(ch)return "".join(chars)def render_token(t: bytes) -> str:"""将字节流转换成str 并去除控制字符Example: 0x68 0x65 0x6c 0x6c 0x6f 0x20 0x0a 0x20 0x77 0x6f 0x72 0x6c 0x64 -> hello \u000a world:params t: bytes 字节流:return s: str 字符串"""s = t.decode(encoding="utf-8", errors="replace")s = replace_control_charactors(s)return sclass Tokenizer():"""Base class for Tokenizers"""def __init__(self):"""Attributes:merges (dict): 存储合并的对和新ID的映射。vocab (dict): 存储字典,包含字符及其对应的字节表示。special_tokens(dict): 特殊字符pattern(str): 模式"""self.merges = {}  # (int, int) -> intself.pattern = "" # strself.special_tokens = {} # str -> int, e.g. {{'<|endoftext|>': 100257}}self.vocab = self._build_vocab() # int -> bytesdef train(self, text, vocab_size, verbose=False):raise NotImplementedErrordef encode(self, text):raise NotImplementedErrordef decode(self, ids):raise NotImplementedErrordef _build_vocab(self):vocab = {idx: bytes(idx) for idx in range(256)}for (p0, p1), idx in self.merges.items():vocab[idx] = vocab[p0] + vocab[p1]for special, idx in self.special_tokens.items():vocab[idx] = special.encode("utf-8", errors="replace")return vocabdef save(self, file_prefix):# 保存模型文件,用于导入 model_file = file_prefix + ".model"with open(model_file, 'w') as f:f.write("minbpe v1\n")f.write(f"{self.pattern}\n")f.write(f"{len(self.special_tokens.items())}\n")for special, idx in self.special_tokens.items():f.write(f"{special} {idx}\n")for idx1, idx2 in self.merges:f.write(f"{idx1} {idx2}\n")# 保存vocab 用于人工检查vocab_file = file_prefix + ".vocab"inverted_merges = {idx: pair for pair, idx in self.merges.items()}with open(vocab_file, 'w', encoding="utf-8") as f:for idx, token in self.vocab.items():s = render_token(token)if idx in inverted_merges:idx0, idx1 = inverted_merges[idx]s0 = render_token(self.vocab[idx0])s1 = render_token(self.vocab[idx1])f.write(f"[{s0}][{s1}] -> [{s}] {idx}\n")else:f.write(f"[{s}] {idx}\n")def load(self, model_file):assert model_file.endswith(".model")# 读取model文件merges = {}special_tokens = {}idx = 256with open(model_file, 'r') as f:version = f.readline().strip()assert version == "minbpe v1"self.pattern = f.readline().strip()num_special = int(f.readline().strip())for _ in range(num_special):special, special_idx = f.readline().strip().split()special_tokens[special] = int(special_idx)for line in f:idx1, idx2 = map(int, line.split())merges[(idx1, idx2)] = idxidx += 1self.merges = mergesself.special_tokens = special_tokensself.vocab = self._build_vocab()

Basic

接下来我们按照上面的BPE算法,不考虑特殊字符与正则化分割,创建一个最基本的Tokenizer类。

from base import Tokenizer, get_stats, mergeclass BasicTokenizer(Tokenizer):"""最简单的BPE进行分词"""def __init__(self):super().__init__()def train(self, text, vocab_size, verbose=False):"""对text进行训练,通过BPE得到mergeparams text: str, 文本训练内容params vocab_size: int(>=256), 得到merge个数为(vocab_size - 256)params verbose: bool, 是否打印"""assert vocab_size >= 256num_merges = vocab_size - 256idx = 256text_bytes = text.encode("utf-8", "replace")ids = list(text_bytes)vocab = {i : bytes([i]) for i in range(256)}merges = {}for i in range(num_merges):new_id = idx + istats = get_stats(ids)pair = max(stats, key=stats.get)merges[(pair)] = new_idids = merge(ids, pair, new_id)vocab[new_id] = vocab[pair[0]] + vocab[pair[1]]if verbose:print(f"merge {i + 1}/{num_merges}:{pair} -> {idx}erges: {pair} -> {idx} ({vocab[idx]}) had {stats[pair]} occurrences")self.merges = mergesself.vocab = vocabdef encode(self, s):s_bytes = s.encode(encoding="utf-8", errors="replace")ids = list(s_bytes)while len(ids) >= 2:stats = get_stats(ids)# merge需要按照训练时的先后顺序pair = min(stats, key=lambda p: self.merges.get(p, float("inf")))if pair not in self.merges:breakids = merge(ids, pair, self.merges[pair])return idsdef decode(self, ids):t = b"".join(self.vocab[i] for i in ids)return t.decode(encoding="utf-8", errors="replace")if __name__ == "__main__":tokenizer = BasicTokenizer()text = "aaabdaaabac"tokenizer.train(text, 256+3)print(tokenizer.encode(text))print(tokenizer.decode([258, 100, 258, 97, 99]))tokenizer.save("toy")

regex

接下来又是一个重要的知识点。即在BPE算法中,我们希望一些字节对永远不要出现,因而我们需要利用regex库提前对于regex进行分割。同时需要对于特殊字符进行一定的处理。分割原理如下:

import regex as re
gpt2pat = re.compile(r"""'s|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+""")print(re.findall(gpt2pat, "Hello've world123 how's are you!!!?"))
# ['Hello', "'ve", ' world', '123', ' how', "'s", ' are', ' you', '!!!?']

然后同样的,我们则是多了一个循环,对于每一个块进行统计与字节对替换的操作。以此为基础创建regex类。

import regex as re
from base import Tokenizer, get_stats, merge# the main GPT text split patterns, see
# https://github.com/openai/tiktoken/blob/main/tiktoken_ext/openai_public.py
GPT2_SPLIT_PATTERN = r"""'(?:[sdmt]|ll|ve|re)| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+"""
GPT4_SPLIT_PATTERN = r"""'(?i:[sdmt]|ll|ve|re)|[^\r\n\p{L}\p{N}]?+\p{L}+|\p{N}{1,3}| ?[^\s\p{L}\p{N}]++[\r\n]*|\s*[\r\n]|\s+(?!\S)|\s+"""class RegexTokenizer(Tokenizer):"""添加正则化和特殊令牌的分词器"""def __init__(self, pattern=None):super().__init__()self.pattern = GPT4_SPLIT_PATTERN if pattern is None else patternself.compiled_pattern = re.compile(pattern=self.pattern)self.special_tokens = {}    # str -> int, example: {'<|endoftext|>': 100257}self.inverse_special_tokens = {}def train(self, text, vocab_size, verbose=False):ids_chunks = [list(ck.encode("utf-8", errors="replace") for ck in self.compiled_pattern.findall(text))]vocab_size = 256 + 3num_merges = vocab_size - 256vocab = {i: bytes([i]) for i in range(256)}merges = {}idx = 256for i in range(num_merges):new_id = idx + istats = {}for chunk in ids_chunks:if len(chunk) >= 2:stats = get_stats(chunk, stats)pair = max(stats, key=stats.get)ids_chunks = [merge(chunk, pair, new_id) for chunk in ids_chunks]merges[pair] = new_idvocab[new_id] = vocab[pair[0]] + vocab[pair[1]]if verbose:print(f"{pair} -> {new_id}")self.merges = mergesself.vocab = vocabdef register_special_tokens(self, special_tokens):self.special_tokens = special_tokensself.inverse_special_tokens = {v: k for k, v in special_tokens.items()}def decode(self, ids):part_bytes = []for idx in ids:if idx in self.vocab:part_bytes.append(self.vocab[idx])elif idx in self.inverse_special_tokens:part_bytes.append(self.inverse_special_tokens[idx].encode(encoding="utf-8", errors="replace"))else:raise ValueError(f"invalid token id: {idx}")text_bytes = b"".join(part_bytes)text = text_bytes.decode(encoding="utf-8", errors="replace")return textdef _encode_chunk(self, text_bytes):"""就是正常的encode,只不过这里没有对于special tokens的处理"""ids = list(text_bytes)while len(ids) >= 2:stats = get_stats(ids)pair = min(stats, key=lambda p: self.merges.get(p, float("inf")))if pair not in self.merges:breakidx = self.merges[pair]ids = merge(ids, pair, idx)return idsdef encode_ordinary(self, text):"""Encoding that ignores any special tokens."""text_chunks = re.findall(self.compiled_pattern, text)ids = []for chunk in text_chunks:chunk_bytes = chunk.encode("utf-8") # raw byteschunk_ids = self._encode_chunk(chunk_bytes)ids.extend(chunk_ids)return idsdef encode(self, text, allowed_special="none_raise"):# decode the user desire w.r.t. handling of special tokensspecial = Noneif allowed_special == "all":special = self.special_tokenselif allowed_special == "none":special = {}elif allowed_special == "none_raise":special = {}assert all(token not in text for token in self.special_tokens)elif isinstance(allowed_special, set):special = {k: v for k, v in self.special_tokens.items() if k in allowed_special}else:raise ValueError(f"allowed_special={allowed_special} not understood")if not special:# shortcut: if no special tokens, just use the ordinary encodingreturn self.encode_ordinary(text)special_pattern = "(" + "|".join(re.escape(k) for k in special) + ")"special_chunks = re.split(special_pattern, text)ids = []for part in special_chunks:if part in special:ids.append(special[part])else:ids.extend(self.encode_ordinary(part))return ids

regex基本实现了GPT4的简易功能。当然还是有一些不同,欢迎大家去Karpathy的Github仓库看一下他的代码。我太懒了,最后的gpt4tokenizer没有实现。

以上则是个人总结的所有内容。欢迎大家交流讨论~

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

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

相关文章

使离医院最远的村庄到医院的路程最短

给定n个村庄之间的交通图&#xff0c;若村庄i和j之间有道路&#xff0c;则将顶点i和j用边连接&#xff0c;边上的Wij表示这条道路的长度&#xff0c;现在要从这n个村庄中选择一个村庄建一所医院&#xff0c;问这所医院应建在哪个村庄&#xff0c;才能使离医院最远的村庄到医院的…

HCIP--以太网交换安全(三)MAC地址漂移防止与检测

MAC地址漂移防止与检测 一、MAC地址漂移防止与检测知识点 1.1MAC地址漂移的概述 MAC地址漂移是指交换机上一个vlan内有两个端口学习到同一个MAC地址&#xff0c;后学习到的MAC地址表项覆盖原MAC地址表项的现象。 1.2.MAC地址漂移的防止方法 &#xff08;1&#xff09;配置…

Windows7 X64 成功安装 .NET Framework 4.8 的两种方法

Windows7 X64 成功安装 .NET Framework 4.8 的两种方法 windows7系统SP1安装完成后&#xff0c;在安装某软件时&#xff0c;提示需要先安装4.6以上的版本net-framework包&#xff0c;正好电脑里有个net-framework4.8软件包&#xff0c;于是打算用上&#xff0c;可是在安装时&a…

Github 2024-10-11 Java开源项目日报 Top9

根据Github Trendings的统计,今日(2024-10-11统计)共有9个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量Java项目9TypeScript项目1Vue项目1C++项目1JeecgBoot 企业级低代码开发平台 创建周期:2062 天开发语言:Java, Vue协议类型:Apache License 2.…

LabVIEW激光诱导击穿光谱识别与分析系统

LabVIEW激光诱导击穿光谱&#xff08;LIBS&#xff09;分析系统利用高能量脉冲激光产生高温等离子体&#xff0c;通过分析等离子体发出的光谱来定性分析样品中的元素种类。该系统的开发集成了软件与硬件的设计&#xff0c;实现了自动识别和定性分析功能&#xff0c;适用于环境监…

OpenCV 之 实现基于Lucas-Kanade算法的光流追踪

引言 在计算机视觉中&#xff0c;光流是指物体、场景或摄像机之间的相对运动造成的像素变化。光流估计是计算机视觉中的一个基础课题&#xff0c;广泛应用于许多领域&#xff0c;比如自动驾驶汽车、机器人导航、手势识别等。OpenCV是一个开源的计算机视觉库&#xff0c;提供了…

计算机网络:计算机网络概述 —— 描述计算机网络的参数

文章目录 数据量性能指标速率带宽数据传输速率 吞吐量时延分析时延问题 时延带宽积往返时间利用率丢包率丢包的情况 抖动可用性可靠性安全性 计算机网络是现代信息社会的基础设施&#xff0c;其性能和可靠性对各类应用至关重要。为了理解和优化计算机网络&#xff0c;我们需要深…

vue后台管理系统从0到1(1)

文章目录 vue后台管理系统从0到1&#xff08;1&#xff09;nvm 下载安装1.卸载nodejs环境2.安装nvm 安装nrm vue后台管理系统从0到1&#xff08;1&#xff09; 第一节主要是先安装我们的工具nvm nodejs版本管理工具&#xff0c;和nrm镜像管理工具 nvm 下载安装 nvm是一款管理…

重学SpringBoot3-集成Redis(一)之基础功能

更多SpringBoot3内容请关注我的专栏&#xff1a;《SpringBoot3》 期待您的点赞&#x1f44d;收藏⭐评论✍ 重学SpringBoot3-集成Redis&#xff08;一&#xff09;之基础功能 1. 项目初始化2. 配置 Redis3. 配置 Redis 序列化4. 操作 Redis 工具类5. 编写 REST 控制器6. 测试 AP…

论文翻译 | Language Models are Few-Shot Learners 语言模型是少样本学习者(上)

摘要 最近的工作表明&#xff0c;通过在大规模文本语料库上进行预训练&#xff0c;然后在特定任务上进行微调&#xff0c;许多自然语言处理&#xff08;NLP&#xff09;任务和基准测试都取得了显著的提升。尽管这种方法在架构上通常是任务无关的&#xff0c;但它仍然需要成千上…

JavaWeb三大组件之Servlet

1. Servlet 一、Servlet介绍 1、概念 Servlet&#xff08;Server Applet&#xff09;是Java Servlet的简称&#xff0c;称为小服务程序或服务连接器&#xff0c;用Java编写的服务器端程序&#xff0c;具有独立于平台和协议的特性&#xff0c;主要功能在于交互式地浏览和生成…

【Python】JSON操作中的高效小窍门

JSON&#xff08;JavaScript Object Notation&#xff09;作为一种轻量级的数据交换格式&#xff0c;在各种应用场景中扮演着重要角色。Python 中处理 JSON 数据非常便捷&#xff0c;主要通过内置的 json 模块来实现。 本文将详细介绍如何使用 Python 进行 JSON 数据的操作&am…

AI与物理学的交汇:Hinton与Hopfield获诺贝尔物理学奖

诺贝尔物理学奖颁给了AI&#xff01;机器学习先驱Hinton与Hopfield联手获奖&#xff0c;出乎所有人的意料。 今年的诺贝尔物理学奖颁给了机器学习领域的两位先驱&#xff0c;杰弗里辛顿&#xff08;Geoffrey Hinton&#xff09;和约翰霍普菲尔德&#xff08;John Hopfield&…

CSS计数器

CSS 中的计数器类似于变量&#xff0c;可以实现简单的计数功能&#xff0c;并将结果显示在页面上&#xff0c;在早期的网站上应用比较广泛。要实现计数器需要用到以下几个属性&#xff1a; counter-reset&#xff1a;创建或者重置计数器&#xff1b;counter-increment&#xf…

【软件部署安装】OpenOffice转换PDF字体乱码

现象与原因分析 执行fc-list查看系统字体 经分析发现&#xff0c;linux默认不带中文字体&#xff0c;因此打开我们本地的windows系统的TTF、TTC字体安装到centos机器上。 安装字体 将Windows的路径&#xff1a; C:\Windows\Fonts 的中文字体&#xff0c;如扩展名为 TTC 与TT…

力扣题31~40

题31&#xff08;中等&#xff09;&#xff1a; 分析&#xff1a; 其实这题题目比较难懂&#xff0c;题目还是挺简单的 我们可以从后面末尾开始&#xff0c;如果前一个大于后面的&#xff0c;说明后面不用动&#xff0c;如果小于&#xff0c;那就找仅仅大于它的数字放前面&…

Chromium 关闭 Google Chrome 后继续运行后台应用功能分析c++

此功能允许关闭 Google Chrome 后继续运行后台&#xff0c;控制此功能的开关是 // Set to true if background mode is enabled on this browser. //更改此值可以修改默认开启关闭 inline constexpr char kBackgroundModeEnabled[] "background_mode.enabled"; …

案例分享—国外优秀UI设计作品赏析

深色UI界面的优点众多&#xff0c;首先体现在视觉舒适度上。深色背景能减少屏幕高亮面积&#xff0c;降低眼部压力&#xff0c;尤其在夜间或光线不足的环境下&#xff0c;深色模式能显著缓解眼睛疲劳&#xff0c;提供更舒适的使用体验。 深色UI界面在设计上更具高端感和优雅氛围…

用Raspberry Pi Imager重装树莓派系统

今天删东西的时候&#xff0c;无意中把系统文件给remove了&#xff0c;结果树莓派无法正常启动&#xff0c;只能重新安装。 用DiskGenius工具将SD卡彻底清空&#xff0c;并将boot分区和文件分区合并为一&#xff0c;之后再对这个新分区进行了格式化。接下来就是烧录镜像了。以…

自动化测试 | 窗口截图

driver.get_screenshot_as_file 是 Selenium WebDriver 的一个方法&#xff0c;它允许你将当前浏览器窗口&#xff08;或标签页&#xff09;的截图保存为文件。这个方法对于自动化测试中的截图验证非常有用&#xff0c;因为它可以帮助你捕获测试执行过程中的页面状态。 以下是…