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

作者:黑夜路人

时间: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_tensorsdef 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 tokenwhile 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_keybigram = min_pairs[min(map(int, min_pairs.keys()))]if not bigram in bpe_ranks:breakbigram = bigram.split(',', 1)first = bigram[0]second = bigram[1]new_word = []i = 0while i < len(word):j = -1try:j = word.index(first, i)except:passif j == -1:new_word.extend(word[i:])breaknew_word.extend(word[i:j])i = jif word[i] == first and i < len(word)-1 and word[i+1] == second:new_word.append(first+second)i += 2else:new_word.append(word[i])i += 1word = new_wordif len(word) == 1:breakpairs = self._get_pairs(word)word = ' '.join(word)self._cache[token] = wordreturn 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)# continuedec_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, w02w10, w11, w12w20, w21, w22w30, w31, w32w40, 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_modeldef 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即公式中的dpe[:, 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)]  # 原向量加上计算出的位置信息才是最终的embeddingreturn self.dropout(x)

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

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

##End##

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

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

相关文章

springboot与rabbitmq的整合【演示5种基本交换机】

前言&#xff1a; &#x1f44f;作者简介&#xff1a;我是笑霸final&#xff0c;一名热爱技术的在校学生。 &#x1f4dd;个人主页&#xff1a;个人主页1 || 笑霸final的主页2 &#x1f4d5;系列专栏&#xff1a;后端专栏 &#x1f4e7;如果文章知识点有错误的地方&#xff0c;…

C语言-ubuntu下的命令

目录 linux命令 【1】打开关闭终端 【2】终端 【3】ls命令 【4】cd 切换路径 【5】新建 【6】删除 【7】复制 【8】移动 【9】常用快捷键 【10】vi编辑器 【11】简单编程步骤 任务&#xff1a; linux命令 【1】打开关闭终端 打开终端&#xff1a; 1. 直接点击 …

数学建模的赛题类型

一、预测类 指通过分析已有的数据或者现象&#xff0c;找出其内在发展规律&#xff0c;然后对未来情形做出预测的过程。 根据已知条件和求解目的&#xff0c;往往将预测类问题分为&#xff1a;小样本内部预测&#xff0c;大样本内部预测。 解决预测类赛题的一般步骤&#xff…

vue3 实现 Map 地图区域组件封装

图例&#xff1a;重庆区域 一、安装echarts 坑&#xff1a;地图echarts版本必须在5.0.0以下&#xff0c;否则不能显示&#xff0c;此处指定安装 echarts4.9.0 即可 cnpm install echarts4.9.0 --save 二、下载 “重庆” 区域地图json文件 下载地址&#xff1a;https://www.…

『表面』在平面模型上提取凸(凹)多边形

原始点云 直通滤波,z轴0~1.1 分割模型为平面&#xff0c;分割结果进行投影 提取多边形 代码: #include <pcl/ModelCoefficients.h> // 模型系数的数据结构&#xff0c;如平面、圆的系数 #include <pcl/io/pcd_io.h>#include <pcl/point_types.h> // 点云数据…

TCP和UDP的区别

连接&#xff1a;TCP 是面向连接的传输层协议&#xff0c;传输数据前先要建立连接&#xff1b;UDP 是不需要连接&#xff0c;即刻传输数据。首部开销&#xff1a;TCP 首部长度较长&#xff0c;首部在没有使用「选项」字段时是 20 个字节&#xff0c;如果使用了「选项」字段则会…

k8s服务发现之第二弹Service详解

创建 Service Kubernetes Servies 是一个 RESTFul 接口对象&#xff0c;可通过 yaml 文件创建。 例如&#xff0c;假设您有一组 Pod&#xff1a; 每个 Pod 都监听 9376 TCP 端口每个 Pod 都有标签 appMyApp apiVersion: v1 kind: Service metadata:name: my-service spec:s…

Modbus TCP/BACnet IP/MQTT物联网网关IOT-810介绍及其典型应用

伴随着计算机技术以及互联网的发展&#xff0c;物联网这个概念已经逐渐进入我们的日常生活&#xff0c;例如智能泊车&#xff0c;智能家居&#xff0c;智能照明&#xff0c;智能楼宇等。智能楼宇是将传统的楼宇自控系统与物联网技术相融合&#xff0c;把系统中常见的传感器、设…

设计模式 ~ 观察者模式

概念 观察者模式是一种设计模式&#xff0c;也被称为发布-订阅模式或事件模式&#xff1b; 用于在对象之间建立一种一对多的依赖关系&#xff0c;当一个对象的状态发生改变时&#xff0c;所有依赖于它的对象都会得到通知并自动更新&#xff1b; ~ 如&#xff1a;DOM事件、vue …

AVLTree深度剖析(单旋)

前言 二叉树搜索树是存在一定的缺陷问题的&#xff0c;当我们要插入的数据是有序&#xff0c;或者说接近于有序&#xff0c;&#xff0c;二叉搜索树及有可能退化为单支树&#xff0c;查找元素相当于在顺序表当中搜索元素&#xff0c;效率低下 --------------------------------…

Python对Excel不同的行分别复制不同的次数

本文介绍基于Python语言&#xff0c;读取Excel表格文件数据&#xff0c;并将其中符合我们特定要求的那一行加以复制指定的次数&#xff0c;而不符合要求的那一行则不复制&#xff1b;并将所得结果保存为新的Excel表格文件的方法。 这里需要说明&#xff0c;在我们之前的文章Pyt…

【C++】list的使用及底层实现原理

本篇文章对list的使用进行了举例讲解。同时也对底层实现进行了讲解。底层的实现关键在于迭代器的实现。希望本篇文章会对你有所帮助。 文章目录 一、list的使用 1、1 list的介绍 1、2 list的使用 1、2、1 list的常规使用 1、2、2 list的sort讲解 二、list的底层实现 2、1 初构…