【原创】实现ChatGPT中Transformer模型之输入处理

news2024/11/25 4:39:33

作者:黑夜路人

时间:2023年7月

Inputs Process(输入处理层)实现

我们看整个绿色框的整个位置,就是Inputs Process(输入处理层)。

在输入处理层,其实非常容易理解,主要就是把输入的每个内容(文字)变成能够跟Encoder交互的,深度学习能够理解识别的东西。

里面主要是两个步骤,一个是对输入字符串进行切分(Tokenize)成为一个个token,另外一个步骤是把token放到一个高纬矩阵中(Input Embedding)的过程,还有一个步骤是为了保证知道输入的token的顺序,所以需要把token位置也进行Embedding(Positional Encoding)。

Embedding就是用一个低维稠密的向量“表示”一个对象,这里所说的对象可以是一个词(Word2vec),也可以是一个物品(Item2vec),亦或是网络关系中的节点(Graph Embedding)。其中“表示”这个词意味着Embedding向量能够表达相应对象的某些特征,同时向量之间的距离反映了对象之间的相似性。Embedding目前在NLP领域有很多模型,不同技术解决方案不同,比如传统的 word2vec,还有后面的FastText、GIoVe、Bert Embedding 等等。

需要做Embedding,首先必须把相关的输入内容(文本)变成一个个token(可以简单理解为搜索里面的分词过程),我们看一个OpenAI GPT Tokenize 结果:

针对整个tokenize和token embedding的大概代码,主要是引用 Transformers 中的bert预训练模型和一些相关词表进行tokenize和embedding的过程:

import torch
from transformers import BertTokenizer, BertModel, AutoTokenizer, AutoModel

# Tokenizer class
class Tokenizer:
    def __init__(self, model_path):
        self.tokenizer = AutoTokenizer.from_pretrained(model_path)

    def tokenize(self, text):
        return self.tokenizer.tokenize(text)
    
    def convert_tokens_to_ids(self, tokens):
        return self.tokenizer.convert_tokens_to_ids(tokens)
    
    def convert_ids_to_tokens(self, ids):
        return self.tokenizer.convert_ids_to_tokens(ids)
    
    def convert_tokens_to_string(self, tokens):
        return self.tokenizer.convert_tokens_to_string(tokens)
    

# Input Sequence Embedding class
class InputEmbedding:
    def __init__(self, model_path):
        self.embedding_model = BertModel.from_pretrained(model_path)
        self.tokenizer       = BertTokenizer.from_pretrained(model_path)

    def get_seq_embedding(self, sequence):
        input_tokens   = self.tokenizer(sequence, return_tensors='pt')
        output_tensors = self.embedding_model(**input_tokens)
        return output_tensors
    
    def get_input_seq_ids(self, sequence):
        return self.tokenizer(sequence, return_tensors='pt')
    
    def get_input_tokens_embedding(self, input_tokens):
        return self.embedding_model(**input_tokens)

大概测试 tokenize和embedding 的代码以及输出:

# define pretrained model path
MODEL_BERT_BASE_ZH          = "D:/Data/Models/roc-bert-base-zh"
MODEL_BERT_BASE_CHINESE      = "bert-base-chinese"
MODEL_BERT_BASE              = "bert-base-cased"

# input sequence list
seqs = [
    '我的名字叫做黑夜路人', 
    'My name is Black',
]

# call transformers tokenizer get tokens and token-ids
tokenizer       = Tokenizer(MODEL_BERT_BASE_ZH)
input_embedding = InputEmbedding(MODEL_BERT_BASE_CHINESE)

for seq in seqs:
    tokens = tokenizer.tokenize(seq)
    print(seq, ' => ', tokens)
    ids = tokenizer.convert_tokens_to_ids(tokens)
    print(seq, ' => ', ids)

    s = input_embedding.get_seq_embedding(seq)
    print(s[0].shape)
    print(s[0])

以上Tokenize和Embedding代码最终输出大概如下结果:

对于Transformer等框架来说,基本在处理 Embedding 场景里,基本主要就是每个字符使用 768 个数字来标识一个token(或者是token id)。比如说,假设常用字有5000个,然后基本都只有一个样本,那么大概会生成一个 tensor.Size([5000, 1, 768]) 的矩阵,就能够在一个高纬矩阵里表示所有的字符了。

Positional Encoding就是句子中词语相对位置的编码,让Transformer保留词语的位置信息。

任何一门语言中,词语的位置和顺序对句子意思表达都是至关重要的。传统的RNN模型在处理句子时,以序列的模式逐个处理句子中的词语,这使得词语的顺序信息在处理过程中被天然的保存下来了,并不需要额外的处理。

而对于Transformer来说,由于句子中的词语都是同时进入网络进行处理,顺序信息在输入网络时就已丢失。因此,Transformer是需要额外的处理来告知每个词语的相对位置的。其中的一个解决方案,就是论文中提到的Positional Encoding,将能表示位置信息的编码添加到输入中,让网络知道每个词的位置和顺序。

如果你对现有训练好的Tokenize不满意,因为这个会对最终的深度学习模型效果产生影响,那么我们可以自己简单做Tokenize的技术实现。

比如我们依赖于GPT-2的BPE词表生成自己的一个Tokenize程序,下面是一个自主实现的Tokenize的核心代码:

    # API: BlackTokenizer.encode(text)
    # 将一个字符串编码成一个整数列表(tokens)
    def encode(self, text):
        """ 
            Transforms a string into an array of tokens 
            :param text: string to be encoded
            :type text: str
            :returns: an array of ints (tokens)
        """
        if not isinstance(text, str):
            text = text.decode(self._DEFAULT_ENCODING)    
        bpe_tokens = []
        matches = self._regex_compiled.findall(text)
        for token in matches:
            token = ''.join([self._byte_encoder[x] for x in self._encode_string(token)])
            new_tokens = [self._encoder[x] for x in self._bpe(token, self._bpe_ranks).split(' ')]
            bpe_tokens.extend(new_tokens)
        return bpe_tokens

    # API: BlackTokenizer.decode(tokens)
    # 将输入的整数列表 tokens 转换成原始字符串
    def decode(self, tokens):
        """ 
            Transforms back an array of tokens into the original string
            :param tokens: an array of ints
            :type tokens: list
            :returns: the original text which was encoded before
        """
        text = ''.join([self._decoder[x] for x in tokens])
        textarr = [int(self._byte_decoder[x]) for x in list(text)]
        text = bytearray(textarr).decode("utf-8")
        return text

里面比较核心的是针对bpe编码和配套encoder.json的处理,BPE文件格式大概是这样:

配套的encoder.json 大概长这样:

处理BPE的核心代码大概是这样的:

   # use BPE algorithm encode input word or phrase
    # 使用 BPE(Byte Pair Encoding)算法将输入的单词或词组进行编码, 该方法只负责对单个单词或词组进行 BPE 编码,如果要对一组文本数据进行 BPE 编码,需要调用 _bpe_batch 方法
    """
    BPE 是一种压缩算法,用于将文本数据中常见的连续字符序列合并成单个字符,以减少词汇量并提高压缩效率

    1. 基于训练数据生成 BPE 码表,即生成常见字母或字符串的组合,并给组合编码一个整数作为标识符。
    2. 将文本中所有的单词划分成字符或者字符组成的子串。
    3. 在所有单词中找出出现次数最多的字符或者字符组合,将这个字符或者字符组合当做一个新的字符来替代原有单词中的这个字符或者字符组合。并在编码表中添加这个字符或者字符组合的编码。
    3. 重复步骤 3 直到达到预设的 BPE 编码次数或者到达最小词频。
    """
    def _bpe(self, token, bpe_ranks):
        if token in self._cache:
            return self._cache[token]
        word = list(token)
        pairs = self._get_pairs(word)
        if not pairs:
            return token

        while True:
            min_pairs = {}
            for pair in pairs:
                pair_key = ','.join(pair)
                rank = bpe_ranks.get(pair_key, float("nan"))
                min_pairs[10e10 if math.isnan(rank) else rank] = pair_key
            bigram = min_pairs[min(map(int, min_pairs.keys()))]
            if not bigram in bpe_ranks:
                break
            bigram = bigram.split(',', 1)
            first = bigram[0]
            second = bigram[1]
            new_word = []
            i = 0

            while i < len(word):
                j = -1
                try:
                    j = word.index(first, i)
                except:
                    pass
                if j == -1:
                    new_word.extend(word[i:])
                    break
                new_word.extend(word[i:j])
                i = j
                if word[i] == first and i < len(word)-1 and word[i+1] == second:
                    new_word.append(first+second)
                    i += 2
                else:
                    new_word.append(word[i])
                    i += 1

            word = new_word
            if len(word) == 1:
                break
            pairs = self._get_pairs(word)
        
        word = ' '.join(word)
        self._cache[token] = word
        return word

基于以上的Tokenize,我们通过一段测试代码:

seqs = [
    '我的名字叫做黑夜路人', 
    'My name is Black',
    "我的nickname叫heiyeluren",
    "はじめまして",
    "잘 부탁 드립니다",
    "До свидания!",
    "😊😁😄😉😆🤝👋",
    "今天的状态很happy,表情是😁",
]

print('\n------------------BlackTokenize Test------------------')

tk = BlackTokenize()
for seq in seqs:
    token_list = tk.get_token_list(seq)
    # print('Text:', seq, ' => Tokens:', tokens)
    enc_seq = tk.encode(seq)
    # continue
    dec_seq = tk.decode(enc_seq)
    token_count = tk.count_tokens(seq)
    print( 'RawText:', seq, ' => TokenList:', token_list, ' => TokenIDs', enc_seq, ' => TokenCount:', token_count, '=> DecodeText:', dec_seq)

print('------------------BlackTokenize Test------------------\n')

测试代码输出结果:

输出结果可以看到,本质就是把不同的字符或者是字符串转成了一个或多个int类型的数字编码,整个Tokenize的过程算完成。

除了Tokenize(token to id)的过程,还有就是Embedding的过程,对于Transformer来说,主要包含词嵌入和位置嵌入两个环节。

我们实现一下这个输入嵌入(Inputs Embedding)的核心代码:

# 输入嵌入(Input Embeddings)层的构建
'''
这个层的作用是将 tokens 的整数列表编码成相应的向量集合,以便后续可以输入到神经网络中.
为了解决能够体现词与词之间的关系,使得意思相近的词有相近的表示结果,这种方法即 Word Embedding(词嵌入)。
最方便的途径是设计一个可学习的权重矩阵 W,将词向量与这个矩阵进行点乘,即得到新的表示结果。
假设 “爱” 和 “喜欢” 这两个词经过 one-hot 后分别表示为 10000 和 00001,权重矩阵设计如下:
[ w00, w01, w02
  w10, w11, w12
  w20, w21, w22
  w30, w31, w32
  w40, w41, w42 ]
那么两个词点乘后的结果分别是 [w00, w01, w02] 和 [w40, w41, w42],在网络学习过程中(这两个词后面通常都是接主语,如“你”,“他”等,或者在翻译场景,
它们被翻译的目标意思也相近,它们要学习的目标一致或相近),权重矩阵的参数会不断进行更新,从而使得 [w00, w01, w02] 和 [w40, w41, w42] 的值越来越接近。
我们还把向量的维度从5维压缩到了3维。因此,word embedding 还可以起到降维的效果。
另一方面,其实,可以将这种方式看作是一个 lookup table:对于每个 word,进行 word embedding 就相当于一个lookup操作,在表中查出一个对应结果。
'''
class Embeddings(nn.Module):
    def __init__(self, d_model, vocab):
        super(Embeddings, self).__init__()

        self.lut = nn.Embedding(vocab, d_model)
        self.d_model = d_model

    def forward(self, x):
        return self.lut(x) * math.sqrt(self.d_model)

另外把相应位置做嵌入:

# 实现的是 Transformer 模型中的位置编码(Positional Encoding)
'''
word embedding,我们获得了词与词之间关系的表达形式,但是词在句子中的位置关系还无法体现。
由于 Transformer 是并行地处理句子中的所有词,因此需要加入词在句子中的位置信息,结合了这种方式的词嵌入就是 Position Embedding
预定义一个函数,通过函数计算出位置信息,大概公式如下:
\begin{gathered}
PE_{(pos,2i)}=\sin{(pos/10000^{2i/d})} \\
P E_{(p o s,2i+1)}=\operatorname{cos}\left(p o s_{\substack{i=1}{\mathrm{osc}}/\mathrm{1}{\mathrm{999}}\mathrm{2}i/d\right) 
\end{gathered}
Transformer 模型使用自注意力机制来处理序列数据,即在编码器和解码器中分别使用自注意力机制来学习输入数据的表示。
由于自注意力机制只对序列中的元素进行注意力权重的计算,它没有固定位置的概念,
因此需要为序列中的元素添加位置信息以帮助 Transformer 模型学习序列中元素的位置。
'''
class PositionalEncoding(nn.Module):
    def __init__(self, d_model, dropout, max_len=5000):
        super(PositionalEncoding, self).__init__()

        self.dropout = nn.Dropout(p=dropout)  
        pe = torch.zeros(max_len, d_model)  # max_len代表句子中最多有几个词
        position = torch.arange(0, max_len).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model))  # d_model即公式中的d
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        pe = pe.unsqueeze(0)
        self.register_buffer('pe', pe)

    def forward(self, x):
        x = x + self.pe[:, :x.size(1)]  # 原向量加上计算出的位置信息才是最终的embedding
        return self.dropout(x)

实际中Tokenize有很多实现方法,可以用已经预训练好的模型直接调用,或者是现成的各种包进行实现。比如 bert tokenize、spacy Tokenize、tiktoken、gpt3_tokenizer等等都可以,只是中英文处理效果不同,或者是最后的token数量切割大小不同,当然,这个最终也会影响训练效果。

取代你的不是AI,而是比你更了解AI和更会使用AI的人!

##End##

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

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

相关文章

Proxy代理前后,Httpheader 的变化

Vite.config 配置Proxy服务器, 解决Rest API 访问SpringBoot接口时&#xff0c;跨域的Session一致性。 import { defineConfig, loadEnv } from viteexport default defineConfig({server: {proxy: {/rest: {target: loadEnv(, process.cwd()).VITE_API_URL,changeOrigin: tru…

分类预测 | MATLAB实现基于Attention-GRU的数据多特征分类预测(门控循环单元融合注意力机制分类预测,含混淆矩阵图、分类图)

分类预测 | MATLAB实现基于Attention-GRU的数据多特征分类预测(门控循环单元融合注意力机制分类预测&#xff0c;含混淆矩阵图、分类图) 目录 分类预测 | MATLAB实现基于Attention-GRU的数据多特征分类预测(门控循环单元融合注意力机制分类预测&#xff0c;含混淆矩阵图、分类图…

python_day9_面向对象

# 设计一个类&#xff08;设计一张登记表&#xff09; class Student:name Nonegender Nonenative Noneage None# 创建一个对象&#xff08;打印一张登记表&#xff09; stu_1 Student() # 对象属性赋值&#xff08;填表&#xff09; stu_1.name jay stu_1.gender "…

Django实现接口自动化平台(十二)自定义函数模块DebugTalks 序列化器及视图【持续更新中】

上一章&#xff1a; Django实现接口自动化平台&#xff08;十一&#xff09;项目模块Projects序列化器及视图【持续更新中】_做测试的喵酱的博客-CSDN博客 本章是项目的一个分解&#xff0c;查看本章内容时&#xff0c;要结合整体项目代码来看&#xff1a; python django vue…

AI:Illustrator 2023 for mac

illustrator是一款矢量图形编辑软件&#xff0c;用于创建和编辑排版、图标、标志、插图和其他类型的矢量图形。 以下是illustrator的几个特点&#xff1a; 矢量图形&#xff1a;illustrator创建的图形是矢量图形&#xff0c;可以无限放大而不失真&#xff0c;这与像素图形编辑…

Linux Lite 6.6发行版的候选版(RC)可供公众测试

导读Linux Lite的创建者Jerry Bezencon近日宣布&#xff0c;即将推出的Linux Lite 6.6发行版的候选版&#xff08;RC&#xff09;开发版本可供公众测试。 Linux Lite 6.6仍然基于Ubuntu 22.04.2 LTS&#xff08;Jammy Jellyfish&#xff09;长期支持的操作系统系列&#xff0c;…

数据结构0X-线性结构-链表栈队列

文章目录 参考&#xff1a;总结大纲要求线性结构-链表 参考&#xff1a; 总结 本系列为C数据结构系列&#xff0c;会介绍 线性结构&#xff0c;简单树&#xff0c;特殊树&#xff0c;简单图等。本文为线性结构部分。 大纲要求 线性结构 【 3 】链表&#xff1a;单链表、双向…

truffle 进行智能合约测试

0字 本方法使用了可视化软件Ganache 前两步与不使用可视化工具的步骤是一样的&#xff08;有道云笔记&#xff09;&#xff0c;到第三步的时候需要注意&#xff1a; 在truffle插件下找到networks目录&#xff0c;提前打开Ganache软件 在Ganache中选择连接或者新建&#xff0…

动手学深度学习v2 p2 线性神经网络 线性回归

3. 线性神经网络 回归&#xff08;regression&#xff09;是能为一个或多个自变量与因变量之间关系建模的一类方法。 在自然科学和社会科学领域&#xff0c;回归经常用来表示输入和输出之间的关系。 在机器学习领域中的大多数任务通常都与预测&#xff08;prediction&#xf…

科技云报道:数字化转型完成后,制造业如何走向“数智”时代?

科技云报道原创。 随着我国数字化转型行动的深入推进和智能制造工程的大力实施&#xff0c;制造业正朝着“数智”时代迈进&#xff0c;生成式AI被视为推动制造业智能化发展的关键驱动力。 据预测&#xff0c;到2027年&#xff0c;将有30%的制造业采用生成式AI来提升产品研发效…

前端Vue仿微信我的菜单栏组件按钮组件

随着技术的发展&#xff0c;开发的复杂度也越来越高&#xff0c;传统开发方式将一个系统做成了整块应用&#xff0c;经常出现的情况就是一个小小的改动或者一个小功能的增加可能会引起整体逻辑的修改&#xff0c;造成牵一发而动全身。 通过组件化开发&#xff0c;可以有效实现…

谷歌 Bard 深夜更新:支持中文、语音输入/播报、代码导出、对话分享

“ 降维打击&#xff1a;你永远想象不到自己的对手会是谁&#xff0c;干掉我们的不一定是同行。谷歌会被ChatGPT 打败吗&#xff1f;” 01 — 两天前&#xff0c;Bard&#xff01;谷歌对 ChatGPT 的最强反击&#xff0c;悄咪咪的支持中文了&#xff01;。今天谷歌官方公布了更新…

ModaHub魔搭社区:AI原生云向量数据库Zilliz Cloud设置运维窗口和设置时区

目录 概述 查看运维窗口 编辑运维窗口 相关文档 查看当前时区 编辑时区 相关文档 Zilliz Cloud 允许用户为集群设置运维窗口,以减少运维对工作负载的影响,增加可预测性。 概述 目前,运维窗口设置为全局设置,应用于 Zilliz Cloud 上的所有集群。 为避免在业务高峰期…

基于 rsync 像 macOS 的 Time Machine 一样备份您的 Linux 服务器

一、前言 Time Machine 这东西用过 macOS 的都知道&#xff0c;可以说是数据备份神器。 前几天我误删了几个文件&#xff0c;还好我有设置 Time Machine 自动备份&#xff0c;于是我打开 Time Machine&#xff0c;点几下鼠标&#xff0c;文件就回来了&#xff01; 我想要是 …

【Docker】Docker镜像和Docker容器

文章目录 Docker镜像镜像基本概念为什么需要镜像&#xff1f;Union FS&#xff08;联合文件挂载&#xff09;docker镜像原理 Docker镜像命令docker rmidocker savedocker loaddocker historydocker image prune docker镜像实战离线迁移镜像镜像存储的压缩与共享 Docker容器容器…

npm link 实现全局运行package.json中的指令

packages.json "name":"testcli","bin": {"itRun": "index.js"},执行命令 npm link如果要解绑定 npm unlink testcli 现在你可以输入 itRun试一下

vue3使用下载附件功能

效果&#xff1a; 点击即可以下载打开。 代码&#xff1a; <div v-show"item.attachment.length > 0"><h3>下载附件</h3><divv-for"(doc, docIndex) in item.attachment":key"docIndex"><astyle"color: #41…

WebSocket集群解决方案,不用MQ

首先不了解WebSocket的可以先看看这篇文章&#xff0c;以及传统的WebSocket方案是怎么做的&#xff0c;https://www.cnblogs.com/jeremylai7/p/16875115.html 这是用MQ解决的版本&#xff0c;那么这种方案存在什么问题呢。 第一&#xff1a;增加MQ&#xff0c;可能造成消息挤压…

动态内存分配(3)——柔性数组

前言&#xff1a; 以前我们所学数组&#xff08;包括变长数组&#xff09;&#xff0c;在数组声明的时候&#xff0c;就必须指定数组的大小&#xff0c;它所需要的内存在编译时分配。但是有时候需要的数组大小在程序运行的时候才能知道&#xff0c;该怎么办呢&#xff1f;这就是…

Zookeeper+kafka的应用及部署

Zookeeperkafka的应用及部署 一、Zookeeper的概念1、Zookeeper 定义2、Zookeeper 工作机制3、Zookeeper 特点4、Zookeeper 数据结构5、Zookeeper 应用场景6、Zookeeper 选举机制&#xff08;1&#xff09;第一次启动选举机制&#xff08;2&#xff09;非第一次启动选举机制(1)、…