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 count
    for (p0, p1) in zip(ids, ids[1:]):
        count[(p0, p1)] = count.get((p0, p1), 0) + 1
    
    return count

counts = 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 = 0
    while 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 + 2
        else:
            new_ids.append(ids[i])
            i = i + 1
    
    return new_ids

new_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) -> int

text = "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_id
    print(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:
            break
        tokens = merge(tokens, pair, merges[pair])

    return tokens

大家可以自己尝试一下。

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

Base

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

import unicodedata


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 count
    for (p0, p1) in zip(ids, ids[1:]):
        count[(p0, p1)] = count.get((p0, p1), 0) + 1
    
    return count


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 = 0
    while 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 + 2
        else:
            new_ids.append(ids[i])
            i = i + 1
    
    return new_ids


def 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 s


class Tokenizer():
    """Base class for Tokenizers"""


    def __init__(self):
        """
        Attributes:
            merges (dict): 存储合并的对和新ID的映射。
            vocab (dict): 存储字典,包含字符及其对应的字节表示。
            special_tokens(dict): 特殊字符
            pattern(str): 模式
        """
        self.merges = {}  # (int, int) -> int
        self.pattern = "" # str
        self.special_tokens = {} # str -> int, e.g. {{'<|endoftext|>': 100257}}
        self.vocab = self._build_vocab() # int -> bytes
    
    def train(self, text, vocab_size, verbose=False):
        raise NotImplementedError
    
    def encode(self, text):
        raise NotImplementedError
        
    def decode(self, ids):
        raise NotImplementedError

    def _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 vocab
    
    def 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 = 256

        with 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)] = idx
                idx += 1
        
        self.merges = merges
        self.special_tokens = special_tokens
        self.vocab = self._build_vocab()

Basic

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

from base import Tokenizer, get_stats, merge


class BasicTokenizer(Tokenizer):
    """
    最简单的BPE进行分词

    
    """

    def __init__(self):
        super().__init__()
    
    def train(self, text, vocab_size, verbose=False):
        """
        对text进行训练,通过BPE得到merge

        params text: str, 文本训练内容
        params vocab_size: int(>=256), 得到merge个数为(vocab_size - 256)
        params verbose: bool, 是否打印
        """

        assert vocab_size >= 256
        num_merges = vocab_size - 256
        idx = 256
        
        text_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 + i
            stats = get_stats(ids)

            pair = max(stats, key=stats.get)

            merges[(pair)] = new_id
            ids = 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 = merges
        self.vocab = vocab

    def 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:
                break

            ids = merge(ids, pair, self.merges[pair])

        return ids
    
    def 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 pattern
        self.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 + 3
        num_merges = vocab_size - 256
        vocab = {i: bytes([i]) for i in range(256)}
        merges = {}

        idx = 256

        for i in range(num_merges):
            new_id = idx + i
            stats = {}
            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_id
            vocab[new_id] = vocab[pair[0]] + vocab[pair[1]]
            
            if verbose:
                print(f"{pair} -> {new_id}")

        self.merges = merges
        self.vocab = vocab

    def register_special_tokens(self, special_tokens):
        self.special_tokens = special_tokens
        self.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 text
    
    def _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:
                break

            idx = self.merges[pair]
            ids = merge(ids, pair, idx)
        return ids
    
    def 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 bytes
            chunk_ids = self._encode_chunk(chunk_bytes)
            ids.extend(chunk_ids)
        return ids
    
    def encode(self, text, allowed_special="none_raise"):
        # decode the user desire w.r.t. handling of special tokens
        special = None
        if allowed_special == "all":
            special = self.special_tokens
        elif 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 encoding
            return 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没有实现。

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

springboot 整合 rabbitMQ(1)

目录 一、MQ概述 二、MQ的优势和劣势 三、常见的MQ产品 RabbitMQ使用步骤 第一步&#xff1a;确保rabbitmq启动并且可以访问15672 第二步&#xff1a;导入依赖 第三步&#xff1a;配置 auto自动确认 manual手工确认&#xff08;推荐使用&#xff01;可以防止消息丢失&a…

东华大学《2023年+2019年824自动控制原理真题》 (完整版)

本文内容&#xff0c;全部选自自动化考研联盟的&#xff1a;《东华大学824自控考研资料》的真题篇。后续会持续更新更多学校&#xff0c;更多年份的真题&#xff0c;记得关注哦~ 目录 2023年真题 2019年真题 Part1&#xff1a;2023年2019年完整版真题 2023年真题 2019年真题…

人工智能AI等级划分

人工智能等级划分 第一级“聊天机器人”。 第二级“推理者”水平。这一级别的AI系统具备类似拥有博士学位教育但未配备任何工具的人类&#xff0c;能执行基础的问题解决任务。据悉&#xff0c;OpenAI的管理层在会议中还向员工们展示了涉及GPT-4 AI模型的一个研究项目&#xff…

ip地址距离多远会变城市

在探讨“IP地址距离多远会变城市”这一话题时&#xff0c;我们首先需要理解几个核心概念&#xff1a;IP地址、地理位置定位以及网络架构的基本原理。这一问题实质上触及了互联网通信的底层逻辑与地理位置信息服务的交集。 一、IP地址与地理位置的关联 IP地址&#xff0c;即互联…

【计算机方向】三本计算机视觉IEEE系列,发文量高,影响因子呈上升趋势,备受国人追捧!

本期将为您带来三本计算机SCI 妥妥毕业神刊&#xff01; IEEE Transactions on Pattern Analysis and Machine Intelligence IEEE Transactions on Knowledge and Data Engineering IEEE Transactions on Cognitive and Developmental Systems 期刊名称&#xff1a;IEEE Tr…

【YOLOv11】ultralytics最新作品yolov11 AND 模型的训练、推理、验证、导出 以及 使用

​目录 一 ultralytics公司的最新作品YOLOV11 1 yolov11的创新 2 安装YOLOv11 3 PYTHON Guide 二 训练 三 验证 四 推理 五 导出模型 六 使用 文档&#xff1a;https://docs.ultralytics.com/models/yolo11/ 代码链接&#xff1a;https://github.com/ultralytics/ult…

【MLP-Mixer】核心方法解读

abstract&#xff1a; 我们提出MLP-Mixer架构(或简称“Mixer”)&#xff0c;这是一个具有竞争力但在概念和技术上都很简单的替代方案&#xff0c;它不使用卷积或自关注。相反&#xff0c;Mixer的架构完全基于多层感知器(mlp)&#xff0c;这些感知器可以在空间位置或特征通道上…

JQuery基本操作(二)

遍历 $(选择器).each(function(下标,值){//代码块 });$.each(数组名,function(下标,值){//代码块 }); <body><button> 获得数组下标和值</button> </body> <script>$(function(){$("button").click(function(){var arr [1,2,3,4,5,…

Ansible 工具从入门到使用

1. Ansible概述 Ansible是一个基于Python开发的配置管理和应用部署工具&#xff0c;现在也在自动化管理领域大放异彩。它融合了众多老牌运维工具的优点&#xff0c;Pubbet和Saltstack能实现的功能&#xff0c;Ansible基本上都可以实现。 Ansible能批量配置、部署、管理上千台主…

基于 CSS Grid 的简易拖拉拽 Vue3 组件,从代码到NPM发布(2)- NPM发布、在线示例

这里分享一下本开源项目是如何构建组件库及其如何发布到NPM上的&#xff0c;还有组件库与在线示例的构建有什么差异。 请大家动动小手&#xff0c;给我一个免费的 Star 吧~ 大家如果发现了 Bug&#xff0c;欢迎来提 Issue 哟~ github源码 NPM 示例地址 版本更新信息 这两天抽空…

自动化测试中如何高效进行元素定位!

前言 在自动化测试中&#xff0c;元素定位是一项非常重要的工作。良好的元素定位可以帮助测试人员处理大量的测试用例&#xff0c;加快测试进度&#xff0c;降低工作负担。但是在实际的测试工作中&#xff0c;我们常常遇到各种各样的定位问题&#xff0c;比如元素定位失败、元…

鸿蒙开发之ArkUI 界面篇 三十三 Builder(封装容器)

鸿蒙开发中遇到容器相同、容器下面的子组件相同&#xff0c;就是子组件的文字不同&#xff0c;背景颜色不同&#xff0c;文字颜色不同之类&#xff0c;就可以使用Builder来封装&#xff0c;语法格式如下&#xff1a; 例如下面的界面&#xff1a; Row4个ColumImageText来实现&am…

Python WebSocket 的原理及其应用

Python WebSocket 的原理及其应用 在现代 Web 开发中&#xff0c;实时通信成为了越来越多应用的重要组成部分。尤其是像聊天应用、实时数据更新、在线游戏等场景&#xff0c;服务器与客户端之间的即时数据传输需求非常迫切。在传统的 HTTP 协议中&#xff0c;通信往往是基于请…

麒麟V10系统下的调试工具(网络和串口调试助手)

麒麟V10系统下的调试工具&#xff08;网络和串口调试助手&#xff09; 1.安装网络调试助手mnetassist arm64-main ①在linux下新建一个文件夹 mkdir /home/${USER}/NetAssist②将mnetassist arm64-main.zip拷贝到上面文件夹中&#xff0c;并解压给权限 cd /home/${USER}/Ne…

JS 运算符

目录 1. 赋值运算符 2. 一元运算符 2.1 自增 2.1.1 前置自增 2.1.2 后置自增 2.1.3 前置与后置自增对比 3. 比较运算符 3.1 字符串比较 4. 逻辑运算符 4.1 案例 5. 运算符优先级 1. 赋值运算符 2. 一元运算符 2.1 自增 2.1.1 前置自增 2.1.2 后置自增 2.1.3 前置与后…

Ubuntu安装Apache教程

系统版本&#xff1a;Ubuntu版本 23.04 Ubuntu是一款功能强大且用户友好的操作系统&#xff0c;而Apache是一款广泛使用的Web服务器软件。在Ubuntu上安装Apache可以帮助用户搭建自己的网站或者进行Web开发。为大家介绍如何在Ubuntu上安装Apache&#xff0c;并提供详细的教程和操…

日语学习零基础生活日语口语柯桥外语学校|股票用日语怎么说?

在日语中&#xff0c;“股票”可以说&#xff1a; • 株&#xff08;かぶ&#xff09; 这是最常用的表达方式&#xff0c;直接表示“股票”。 例如&#xff1a; 株を買う - 买股票 株を売る - 卖股票 • 株式&#xff08;かぶしき&#xff09; 这个词也是“股票”的意…

网站集群批量管理-Ansible(ad-hoc)

1. 概述 1. 自动化运维: 批量管理,批量分发,批量执行,维护 2. 无客户端,基于ssh进行管理与维护 2. 环境准备 环境主机ansible10.0.0.7(管理节点)nfs01 10.0.0.31(被管理节点)backup10.0.0.41(被管理节点) 2.1 创建密钥认证 安装sshpass yum install -y sshpass #!/bin/bash ##…

息肉检测数据集 yolov5 yolov8适用于目标检测训练已经调整为yolo格式可直接训练yolo网络

息肉检测数据集 yolov5 yolov8格式 息肉检测数据集介绍 数据集概述 名称&#xff1a;息肉检测数据集&#xff08;基于某公开的分割数据集调整&#xff09;用途&#xff1a;适用于目标检测任务&#xff0c;特别是内窥镜图像中的息肉检测格式&#xff1a;YOLO格式&#xff08;边…

YOLO11改进 | 注意力机制 | 轻量级的空间组增强模块SGE【全网独家】

秋招面试专栏推荐 &#xff1a;深度学习算法工程师面试问题总结【百面算法工程师】——点击即可跳转 &#x1f4a1;&#x1f4a1;&#x1f4a1;本专栏所有程序均经过测试&#xff0c;可成功执行&#x1f4a1;&#x1f4a1;&#x1f4a1; 本文介绍了一个空间组增强&#xff08;S…