动手学LLM(ch2)——文本数据处理

news2024/12/29 11:27:58

前言

在这里,您将学习如何为训练大型语言模型(LLMs)准备输入文本。这包括将文本分割成单个词汇和子词汇token,然后将它们编码成向量表示,供大型语言模型(LLM)使用。您还将了解字节对编码等高级分词方案,这些方案已经在GPT等流行的大型语言模型(LLMs)中得到应用。最后,我们将介绍采样和数据加载策略,这些策略用于生成后续中训练大型语言模型(LLMs)所需的输入输出对。本文内容从以下几个方面展开:

- 为大型语言模型训练准备文本

- 将文本分割为词汇和子词汇token

- 字节对编码是一种更高级的文本分词方法

- 使用滑动窗口方法抽样训练示例

- 将token转换为向量输入大型语言模型

2.1 理解词嵌入

深度神经网络模型,包括大型语言模型(LLMs),无法直接处理原始文本,因为文本是分类数据,与神经网络的数学运算不兼容。为了达到这个目的,需要将单词转换为连续值向量。记住一句话就行,为了兼容神经网络的数学运算,把所有原始数据类型转变为连续值向量就是嵌入(embedding)。

如下图所示,通过特定的神经网络层或预训练模型,可以将视频、音频和文本等不同类型的数据嵌入为密集向量表示,使深度学习架构能够理解和处理这些原始数据。

需要注意的是,不同的数据格式需要各自专用的嵌入模型,例如,针对文本设计的嵌入模型不适用于音频或视频数据。

嵌入本质上是将离散对象(如单词、图像或文档)映射到连续向量空间的过程,目的是将非数字数据转换为神经网络可处理的格式。虽然词嵌入是最常见的形式,但也可以扩展到句子、段落或整个文档。当前,词嵌入的生成有多种算法和框架,其中Word2Vec是早期和流行的选择,它通过预测上下文生成词嵌入,基于相似上下文中词的相似含义。词嵌入可以有不同维度,从一维到数千维,高维度可以捕捉更多细微关系,尽管计算效率较低。大型语言模型(LLMs)通常生成自己的嵌入,优化以适应特定任务和数据。以GPT-2和GPT-3为例,嵌入大小根据模型变种变化,最小的GPT-2和GPT-3使用768维,而最大的GPT-3使用12,288维,这反映了性能与效率之间的权衡。

如下图所示,如果词嵌入是二维的,我们可以将它们绘制在二维散点图中以便于可视化。在使用词嵌入技术,例如 Word2Vec 时,对应于相似概念的词在嵌入空间中通常彼此接近。例如,在嵌入空间中,不同类型的鸟类相对于国家和城市更为靠近。

注意的是,LLMs 还可以创建上下文化的输出嵌入。这个后面会去讨论

2.2 文本分词(序列化)

如何将输入文本分割成单独的token,这是大型语言模型(LLM)创建嵌入的必需预处理步骤。

如下图所示,文本处理步骤在大型语言模型(LLM)中的最开始的阶段。我们将输入文本分割成单独的token,这些token可能是单词或特殊字符,例如标点符号。后续,我们会将把文本转换成token ID 并创建token嵌入。

读取本地的文本文件

这里没什么需要注意的,就是很简单的读取文件的代码。

# 设置要读取的文件路径,这里是本地文件
file_path = "./the-verdict.txt"
# 打开文件并读取内容
with open(file_path, 'r', encoding='utf-8') as file:
    raw_text = file.read()  # 读取文件内容
# 输出文本的总字符数
print("Total number of characters:", len(raw_text))
# 输出文件内容的前99个字符,以便预览
print(raw_text[:99])

使用 Python 的正则表达式库 re 模块来示例说明分词

这里其实看看就行,知道分词后大概什么样子就好了,因为我们后面转用预构建的分词器。

# 设置要读取的文件路径,这里是本地文件
file_path = "./the-verdict.txt"
# 打开文件并读取内容
with open(file_path, 'r', encoding='utf-8') as file:
    raw_text = file.read()  # 读取文件内容
# # 输出文本的总字符数
# print("Total number of characters:", len(raw_text))
# # 输出文件内容的前99个字符,以便预览
# print(raw_text[:99])

import re  # 导入正则表达式模块

# 定义一个包含标点和空格的字符串
text = "Hello, world. This, is a test."

# 使用正则表达式按照空格进行分割,并保留空格作为分割符
result = re.split(r'(\s)', text)
print(result)  # 输出分割结果

# 使用正则表达式按照标点(逗号、句号)和空格进行分割,并保留分割符
result = re.split(r'([,.]|\s)', text)
print(result)  # 输出分割结果

# 去除结果中的空字符串
result = [item.strip() for item in result if item.strip()]
print(result)  # 输出处理后的结果

# 另一个包含不同标点和空格的字符串
text = "Hello, world. Is this-- a test?"

# 使用正则表达式按照标点(逗号、句号、问号、感叹号、括号、单引号)和空格进行分割,并保留分割符
result = re.split(r'([,.?_!"()\']|--|\s)', text)

# 去除结果中的空字符串
result = [item.strip() for item in result if item.strip()]
print(result)  # 输出处理后的结果

preprocessed = re.split(r'([,.?_!"()\']|--|\s)', raw_text)
preprocessed = [item.strip() for item in preprocessed if item.strip()]
print("_"*100)
print(preprocessed[:10])
print(len(preprocessed))

2.3 将token转换为tokenID

如下图所示,我们将文本token转换为tokenID,稍后可以通过嵌入层进行处理。

# 将预处理后的单词列表去重并排序
all_words = sorted(set(preprocessed))
# 计算词汇表的大小
vocab_size = len(all_words)
# 创建一个字典,将每个单词映射到唯一的整数索引
vocab = {token: integer for integer, token in enumerate(all_words)}
# 输出词汇表的大小
print(vocab_size)

# 遍历词汇字典并输出前50个单词及其对应的索引
for i, item in enumerate(vocab.items()):
    print(item)  # 输出单词及其索引
    if i >= 50:  # 当索引达到50时停止输出
        break

封装标记器类

该类实现了一个简单的文本编码和解码器。构造函数接收一个词汇表,将其映射为字符串到整数和整数到字符串的字典。encode 方法将输入文本分割、预处理并转换为整数 ID 列表,decode 方法则将 ID 列表转换回文本,并处理标点前的空格。如下图所示

class SimpleTokenizerV1:
    def __init__(self, vocab):
        # 初始化时接收一个词汇表字典
        self.str_to_int = vocab  # 字符串到整数的映射
        # 创建整数到字符串的反向映射
        self.int_to_str = {i: s for s, i in vocab.items()}

    def encode(self, text):
        # 使用正则表达式分割输入文本,保留标点和空格作为分割符
        preprocessed = re.split(r'([,.:;?_!"()\']|--|\s)', text)

        # 去除每个分割后的项的前后空白,并过滤掉空字符串
        preprocessed = [
            item.strip() for item in preprocessed if item.strip()
        ]
        # 将处理后的每个单词转换为对应的整数ID
        ids = [self.str_to_int[s] for s in preprocessed]
        return ids  # 返回编码后的ID列表

    def decode(self, ids):
        # 根据ID列表生成对应的文本字符串
        text = " ".join([self.int_to_str[i] for i in ids])
        # 替换指定标点前的空格
        text = re.sub(r'\s+([,.?!"()\'])', r'\1', text)
        return text  # 返回解码后的文本
# 创建一个 SimpleTokenizerV1 实例,传入词汇表
tokenizer = SimpleTokenizerV1(vocab)
# 定义要编码的文本
text = """"It's the last he painted, you know," 
           Mrs. Gisburn said with pardonable pride."""

# 使用 tokenizer 的 encode 方法将文本编码为 ID 列表
ids = tokenizer.encode(text)
# 打印编码后的 ID 列表
print("Encoded IDs:", ids)
# 使用 tokenizer 的 decode 方法将 ID 列表解码回文本
decoded_text = tokenizer.decode(ids)
# 打印解码后的文本
print("Decoded text:", decoded_text)
# 再次使用 encode 和 decode 方法,验证编码和解码的一致性
# 编码文本后再解码
consistent_decoded_text = tokenizer.decode(tokenizer.encode(text))
# 打印一致性验证的结果
print("Consistent decoded text:", consistent_decoded_text)

2.4 添加特殊上下文token

在文本处理中,为了提供额外的上下文,添加一些“特殊”标记是非常有用的,特别是在处理未知单词和文本结束时。一些标记化器使用特殊标记来帮助大语言模型(LLM)理解文本,如下图所示。以下是一些常见的特殊标记:

  • [BOS](序列开始)表示文本的开头。
  • [EOS](序列结束)标记文本结束的位置,通常用于连接多段不相关的文本,例如两篇不同的维基百科文章或两本不同的书籍。
  • [PAD](填充)在训练 LLM 时,当批量大小大于 1 时,可能会包含长度不同的多个文本;使用填充标记可以将较短的文本填充到最长的长度,以使所有文本具有相同的长度。
  • [UNK] 用于表示不在词汇表中的单词。 

需要注意的是,GPT-2 不需要上述提到的任何标记,只使用 <|endoftext|> 标记以简化处理,如下图所示。这个 <|endoftext|> 标记类似于 [EOS] 标记。GPT 还使用 <|endoftext|> 进行填充,因为在训练批量输入时通常使用掩码,填充标记不会被关注,因此这些标记的具体内容并不重要。

此外,GPT-2 不使用 <UNK> 标记来处理超出词汇表的单词,而是采用字节对编码(BPE)标记化器,将单词分解为子词单元,这将在后面中讨论。

注意,在进行文本标记化时,如果输入文本中的单词不在词汇表中,例如 "Hello",则会产生错误。为了解决这种情况,可以在词汇表中添加特殊标记,如 "<|unk|>",用于表示未知单词。此外,由于我们已经在扩展词汇表,可以再添加一个名为 "<|endoftext|>" 的标记,它在 GPT-2 的训练中用于表示文本的结束(并且在连接多个文本时也会使用,例如当训练数据集包含多篇文章、书籍等时)。

tokenizer = SimpleTokenizerV1(vocab)
text = "Hello, do you like tea. Is this-- a test?"
tokenizer.encode(text)

为了让标记化器能够正确使用新的 <|unk|> 标记,我们需要对其进行相应的调整。接下来,我们可以尝试使用修改后的标记化器进行标记化

import re  # 导入正则表达式模块

class SimpleTokenizerV2:
    def __init__(self, vocab):
        # 初始化时接收一个词汇表字典
        self.str_to_int = vocab  # 字符串到整数的映射
        # 创建整数到字符串的反向映射
        self.int_to_str = {i: s for s, i in vocab.items()}

    def encode(self, text):
        # 使用正则表达式分割输入文本,保留标点和空格作为分割符
        preprocessed = re.split(r'([,.:;?_!"()\']|--|\s)', text)
        # 去除每个分割后的项的前后空白,并过滤掉空字符串
        preprocessed = [item.strip() for item in preprocessed if item.strip()]
        # 将不在词汇表中的单词替换为 "<|unk|>"
        preprocessed = [
            item if item in self.str_to_int
            else "<|unk|>" for item in preprocessed
        ]

        # 将处理后的每个单词转换为对应的整数ID
        ids = [self.str_to_int[s] for s in preprocessed]
        return ids  # 返回编码后的ID列表

    def decode(self, ids):
        # 根据ID列表生成对应的文本字符串
        text = " ".join([self.int_to_str[i] for i in ids])
        # 替换指定标点前的空格
        text = re.sub(r'\s+([,.:;?!"()\'])', r'\1', text)
        return text  # 返回解码后的文本

# 文件路径
file_path = "./the-verdict.txt"
# 打开文件并读取内容
with open(file_path, 'r', encoding='utf-8') as file:
    raw_text = file.read()  # 读取文件内容

# 对文本进行预处理,分割并去除空白
preprocessed = re.split(r'([,.?_!"()\']|--|\s)', raw_text)
preprocessed = [item.strip() for item in preprocessed if item.strip()]

# 将预处理后的单词列表去重并排序
all_tokens = sorted(set(preprocessed))
# 在词汇表中添加特殊标记
all_tokens.extend(["<|endoftext|>", "<|unk|>"])
# 计算词汇表的大小
vocab_size = len(all_tokens)

# 创建一个字典,将每个单词映射到唯一的整数索引
vocab = {token: integer for integer, token in enumerate(all_tokens)}

# 打印词汇表中最后五个单词及其索引
for i, item in enumerate(list(vocab.items())[-5:]):
    print(item)

# 创建标记化器实例
tokenizer = SimpleTokenizerV2(vocab)

# 定义两个示例文本
text1 = "Hello, do you like tea?"
text2 = "In the sunlit terraces of the palace."
# 连接两个文本,用 "<|endoftext|>" 标记分隔
text = " <|endoftext|> ".join((text1, text2))
print(text)  # 打印连接后的文本

# 使用标记化器进行编码
ids = tokenizer.encode(text)

# 打印编码后的 ID 列表
print("Encoded IDs:", ids)

# 使用标记化器的 decode 方法将 ID 列表解码回文本
decoded_text = tokenizer.decode(ids)
# 打印解码后的文本
print("Decoded text:", decoded_text)

# 再次使用 encode 和 decode 方法,验证编码和解码的一致性
# 编码文本后再解码
consistent_decoded_text = tokenizer.decode(tokenizer.encode(text))
# 打印一致性验证的结果
print("Consistent decoded text:", consistent_decoded_text)

2.5 字节对编码(BPE)

  • GPT-2 使用字节对编码(BytePair Encoding, BPE)作为其标记化器。
  • 这种方法允许模型将不在预定义词汇表中的单词拆分为更小的子词单元,甚至是单个字符,从而能够处理超出词汇表的单词。
  • 例如,如果 GPT-2 的词汇表中没有单词 "unfamiliarword",它可能将其标记化为 ["unfam", "iliar", "word"] 或其他子词拆分,这取决于其训练时的 BPE 合并策略。
  • 原始的 BPE 标记化器代码可以在这里找到:https://github.com/openai/gpt-2/blob/master/src/encoder.py。
  • 在本章中,我们使用了 OpenAI 的开源库 tiktoken 中的 BPE 标记化器,该库的核心算法用 Rust 实现,以提高计算性能。
  • 我在 ./bytepair_encoder 目录中创建了一个笔记本,比较了这两种实现的性能(tiktoken 在样本文本上的速度约快 5 倍)。
# pip install tiktoken

import tiktoken

tokenizer = tiktoken.get_encoding("gpt2")

text = (
    "Hello, do you like tea? <|endoftext|> In the sunlit terraces"
     "of someunknownPlace."
)

integers = tokenizer.encode(text, allowed_special={"<|endoftext|>"})
print(integers)

strings = tokenizer.decode(integers)
print(strings)

基于上述的token IDs以及解码的文本,我们可以做出2点有价值的观察。

第一,`<|endoftext|>`词元(token)被赋值了一个很大的token ID,例如,50256。事实上,被用于训练诸如GPT-2,GPT-3以及被ChatGPT使用的原始模型的BPE分词器,总计词汇的规模是50257,其中`<|endoftext|>`被指定为最大的token ID。  

第二,上述的BPE分词器可以正确的解码和编码没有见过的词汇,例如"someunknownPlace"。BPE解码器可以处理任何没有见过的词汇。那么,他是怎么无需使用`<|unk|>`词元就做到这个的呢?  

BPE使用的算法会将不在预定义词表里的单词分解为更小的子单词单元或者甚至是独立的字母,使BPE可以处理词表外的单词。所以,基于这种算法,如果分词器在分词时遇到了不熟悉的单词,他会使用一系列的子单词词元或者字母来替换它,就像如下图所示,将不认识的单词分解为更小的的词元或字母保证了分词器,以及后续被训练的大模型可以处理任意的文本,即使文本里包含了从来没在测试数据里出现过的单词。对BPE更详细的讨论和实现可以搜搜看看别的博客。

 

 **练习2.1 未知单词的字节对编码**  

尝试使用tiktoken库里的BPE分词器,对未知词汇"Akwirw ier"进行处理然后打印出一列词元编码(token IDs)。然后,针对这一列的每一个编码调用解码函数来重现上图所示的映射关系。最后,在词元ID序列上调用解码方法来确认是否它们可以重新构成原始的输入"Akwirw ier"。  

# test
import tiktoken

# 初始化分词器,这里使用 GPT-2 模型的 BPE 分词器
bpe = tiktoken.get_encoding("gpt2")

# 输入的未知词汇
input_text = "Akwirw ier"

# 对输入进行分词并打印词元ID
token_ids = bpe.encode(input_text)
print(f"词元ID: {token_ids}")

# 针对每一个词元ID,进行解码,查看它们各自代表的字符
print("单个词元ID对应的解码结果:")
for token_id in token_ids:
    print(f"{token_id}: {bpe.decode([token_id])}")

# 对整个词元ID序列进行解码,并验证是否与原始输入匹配
decoded_text = bpe.decode(token_ids)
print(f"解码后的文本: {decoded_text}")

# 验证解码后的文本是否与原始文本一致
if decoded_text == input_text:
    print("解码后的文本与原始输入匹配。")
else:
    print("解码后的文本与原始输入不匹配。")

2.6  使用滑动窗口进行数据采样

在创建 LLM 的 Embedding 之前,我们需要生成训练 LLM 所需的输入-目标(input-target)对。这些输入-目标对是什么样的呢?LLM 是通过预测文本中的下一个单词来进行预训练的,如图 2.1 所示。如下图所示,给定一个文本样本,提取输入块作为 LLM 的输入子样本,LLM 在训练期间的任务是预测输入块之后的下一个单词。在训练过程中,我们会屏蔽掉目标词之后的所有单词。请注意,在 LLM 处理文本之前,该文本已经进行 token 化,为了便于说明,此图省略了 token 化步骤。本小结中实现了一个数据加载器,它使用滑动窗口方法从训练数据集中获取下图中描述的输入-目标对。

 

首先,使用BPE 分词器对整个《The Verdict》短篇故事进行分词。

import tiktoken

tokenizer = tiktoken.get_encoding("gpt2")
with open("the-verdict.txt", "r", encoding="utf-8") as f:
    raw_text = f.read()
enc_text = tokenizer.encode(raw_text)
print(len(enc_text))

对训练集应用 BPE 分词器后获得 5145 个 tokens。 

接下来,从数据集中剔除前 50 个 toekns,以便在后续步骤中展示更吸引人的文本段落。在创建下一个单词预测任务的输入-目标对时,一种简单直观的方法是创建两个变量 x 和 y。其中,x 用于存储输入的 token 序列,而 y 则用于存放目标 token 序列。目标序列由输入序列中的每个 token 向右移动一个位置构成。从而形成了输入-目标对。

enc_sample = enc_text[50:]
context_size = 4
x = enc_sample[:context_size]
y = enc_sample[1:context_size+1]
print(f"x: {x}")
print(f"y:      {y}")

通过将输入数据向右移动一个位置来生成对应的目标数据后。可以上图,按照以下步骤创建下一个单词的预测任务

for i in range(1, context_size+1):
    context = enc_sample[:i]
    desired = enc_sample[i]
    print(context, "---->", desired)

为了便于更好地理解,我们重复之前的代码,但这次我们将 token ID 转换回文本

for i in range(1, context_size+1):
    context = enc_sample[:i]
    desired = enc_sample[i]
    print(tokenizer.decode(context), "---->", tokenizer.decode([desired]))

待续......

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

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

相关文章

通信工程学习:什么是TFTP简单文件传输协议

TFTP&#xff1a;简单文件传输协议 TFTP&#xff08;Trivial File Transfer Protocol&#xff0c;简单文件传输协议&#xff09;是一种轻量级的文件传输协议&#xff0c;主要用于在计算机网络中传输小型文件。以下是对TFTP的详细解释&#xff1a; 一、TFTP简单文件传输协议的定…

无人机专业除理论外,飞手执照、组装、调试实操技术详解

无人机专业的学习除了丰富的理论知识外&#xff0c;飞手执照的获取、无人机的组装与调试等实操技术也是至关重要的。以下是对这些方面的详细解析&#xff1a; 一、无人机飞手执照 1. 必要性 法规要求&#xff1a;根据《民用无人驾驶航空器系统驾驶员管理暂行规定》等相关法规…

HTB:Oopsie[WriteUP]

目录 连接至HTB服务器并开启靶机 1.With what kind of tool can intercept web traffic? 2.What is the path to the directory on the webserver that returns a login page? 3.What can be modified in Firefox to get access to the upload page? 4.What is the acc…

关于TF-IDF的一个介绍

在这篇文章中我将介绍TF-IDF有关的一些知识&#xff0c;包括其概念、应用场景、局限性以及相应的代码。 一、概念 TF-IDF&#xff08;Term Frequency-Inverse Document Frequency&#xff09;是一种广泛用于信息检索和文本挖掘中的统计方法&#xff0c;用于评估一个词在一个文…

线路交换与分组交换的深度解析

1. 线路交换 原理 线路交换是一种在通信双方之间建立固定通信路径的方式。当用户发起通信时&#xff0c;网络为其分配一条专用的物理通道&#xff0c;这条通道在整个通话过程中保持不变。这意味着在通话期间&#xff0c;其他用户无法使用这条线路。 优点 稳定性&#xff1a…

在职场,没人告诉你的人情世故

职场中&#xff0c;想要过得游刃有余&#xff0c;就必须懂一些人情世故和处事原则。今天&#xff0c;给大家分享个人认为非常重要的5点人情世故&#xff0c;希望能帮你在职场里少吃点亏、多份从容。 01 不要空口道谢 在职场中&#xff0c;别人帮了你&#xff0c;口头道谢是基…

【GO语言】卡尔曼滤波例程

本文给出一个简单的卡尔曼滤波的 Go 语言实现示例&#xff0c;以及相应的讲解文档。 源代码 package mainimport ("fmt" )type KalmanFilter struct {x float64 // 状态估计P float64 // 估计误差协方差F float64 // 状态转移矩阵H float64 //…

在2核2G服务器安装部署MySQL数据库可以稳定运行吗?

阿里云2核2G服务器可以安装MySQL数据库吗&#xff1f;当然可以&#xff0c;并且可以稳定运行MySQL数据库&#xff0c;目前阿里云服务器网aliyunfuwuqi.com使用的就是阿里云2核2G服务器&#xff0c;在云服务器上安装MySQL数据库&#xff0c;可以稳定运行。 目前阿腾云用于运行M…

AWS IoT Core for Amazon Sidewalk

目录 1 前言2 AWS IoT2.1 准备条件2.2 创建Credentials2.2.1 创建user2.2.2 配置User 2.3 本地CLI配置Credentials 3 小结 1 前言 在测试Sidewalk时&#xff0c;device发送数据&#xff0c;网关接收到&#xff0c;网关通过网络发送给NS&#xff0c;而此处用到的NS是AWS IoT&am…

html中的文本标签(含标签的实现案例)

目录 1.标题标签 2.标题标签的align属性 3.段落标签 4.水平线标签hr 5.换行标签br 6.文本样式标签font ​编辑7.文本格式化标签 8.文本语义标签 1&#xff09;时间time标签 2&#xff09;文本高亮Mark标签 3&#xff09;cite标签 9.特殊字符标签 10.图像标签img 附录&#xff…

前端登录页面验证码

首先&#xff0c;在el-form-item里有两个div&#xff0c;各占一半&#xff0c;左边填验证码&#xff0c;右边生成验证码 <el-form-item prop"code"><div style"display: flex " prop"code"><el-input placeholder"请输入验证…

SpringSession微服务

一.在linux中确保启动起来redis和nacos 依赖记得别放<dependencyManagement></dependencyManagement>这个标签去了 1.首先查看已经启动的服务 docker ps 查看有没有安装redis和nacos 2.启动redis和nacos 发现没有启动redis和nacos,我们先来启动它。&#xff0c;…

在idea使用nacos微服务

一.安装nacos 、依赖记得别放<dependencyManagement></dependencyManagement>这个标签去了 1.在linux拉取镜像安装 docker pull nacos/nacos-server:1.3.1 2.创建挂载目录 mkdir -p /usr/local/docker/nacos/init.d /usr/local/docker/nacos/logs 3.安装nacos…

数据结构:将复杂的现实问题简化为计算机可以理解和处理的形式

整句话的总体意义是&#xff0c;**数据结构是用于将现实世界中的实体和关系抽象为数学模型&#xff0c;并在计算机中表示和实现的关键工具**。它不仅包括如何存储数据&#xff0c;还包括对这些数据的操作&#xff0c;能够有效支持计算机程序的运行。通过这一过程&#xff0c;数…

netty之NettyServer字符串编码器

前言 netty通信就向一个流水channel管道&#xff0c;我们可以在管道的中间插入一些‘挡板’为我们服务。比如字符串的编码解码&#xff0c;在前面我们使用new StringDecoder(Charset.forName(“GBK”))进行字符串解码&#xff0c;这样我们在收取数据就不需要手动处理字节码。那…

二叉树相关知识

目录 一.基础 1. 定义 2. 二叉树的特点 3. 二叉树的类型 (1) 满二叉树&#xff1a; (2) 完全二叉树&#xff1a; (3) 斜二叉树&#xff1a; (4) 二叉搜索树&#xff08;Binary Search Tree&#xff0c;BST&#xff09; (5)平衡二叉搜索树&#xff08;Balanced Binary Se…

CSS | 面试题:你知道几种移动端适配方案?

目录 一、自适应和响应式 二、为什么要做移动端适配&#xff1f; 三、当前流行的几种适配方案 (1) 方案一&#xff1a;百分比设置&#xff08;不推荐&#xff09; (2) 方案二&#xff1a;rem 动态设置 font-size px 与 rem 的单位换算 手动换算 less/scss函数 webpac…

存储主动防御,为什么Gartner技术曲线尤为重视?

【科技明说 &#xff5c; 科技热点关注】 近来&#xff0c;从Gartner发布的2024年存储技术成熟曲线&#xff08;Hype Cycle for Storage Technologies ,2024&#xff09;的相关报告看出&#xff0c;到2028年&#xff0c;所有存储产品都将融入专注于数据主动防御的网络存储功能&…

PyCharm 社区版(2024.2.3)安装、配置、创建项目

PyCharm 社区版&#xff08;2024.2.3&#xff09;安装、配置、创建项目 本文目录&#xff1a; 零、时光宝盒 一、PyCharm简介 二、PyCharm特点和功能 三、PyCharm的安装 3.1、PyCharm的安装条件 3.2、下载PyCharm 3.3、安装PyCharm 四、配置PyCharm 4.1、PyCharm的汉化…

基础算法--递归算法【难点、重点】

今天我们即将要开始讲解算法中第一块儿难啃地骨头--递归了&#xff0c;相信有不少小伙伴都因递归而迷惑过&#xff0c;本文就来给大家详细的讲解一下递归到底是什么东西。让你也能瞬间将他打回原形。 递归的理解 在学习递归之前&#xff0c;我们先理解递归。什么是递归呢&…