文章目录
- 参考书《多模态大模型:算法、应用与微调》
- 1.分词
- 2.分词算法
- 主流的三种分词算法,BPE分词算法(GPT-2、BART、Llama模型)、WordPiece分词算法(BERT模型)、SentencePiece分词算法(ChatGLM、BLOOM、PaLM模型)
- 3.Seq2Seq结构模型
- 编码器(Encoder)
- 解码器(Decoder)
- 工作流程
- 4.加入注意力机制的Seq2Seq模型
- 5.日期转换实战完整代码(加入注意力机制的Seq2Seq结构模型)
参考书《多模态大模型:算法、应用与微调》
1.分词
词元(token)可以理解为最小的语义单元,分词的目的是将输入文本转换为一系列的词元,并且还要保证每个词元拥有相对完整的独立语义。
分词的粒度从细到粗依次是character、subword、word。
1.character表示的是单个字符,例如a、b、c、d。
2.word表示的是整个单词,例如water表示水的意思。
3.subword相当于英文中的词根、前缀、后缀等,例如unfortunately中的un、fortun(e)、ly等就是subword,它们都是有含义的。
2.分词算法
分词算法也经历了按词语分、按字分和按子词分三个阶段。
1.按词语分和按字分比较好理解,也有一些工具包可以使用,例如jieba分词。
2.如果是按照子词分,那么词元就可以是偏旁部首,当然对于比较简单的字,一个词元也可以是一个完整的字。
例如,“江”“河”“湖”“海”这四个字都跟水有关,并且它们都是三点水旁,那么在分词的时候,“氵”很可能会作为一个词元,“工”“可”“胡”“每”是另外的词元。
假如“氵”的词元ID为1,“工”“可”“胡”“每”的词元ID分别是2、3、4、5,那么“江”“河”“湖”“海”的词元序列就可以表示为12、13、14、15。
这样做的好处是,只要字中带有三点水旁,或者词元序列中含有词元ID为1的元素,那么我们就可以认为这个字或者这个词元序列跟水有关。即使是沙漠的“沙”字,是由“氵”和“少”组成的,也可以理解为水很少的地方。
主流的三种分词算法,BPE分词算法(GPT-2、BART、Llama模型)、WordPiece分词算法(BERT模型)、SentencePiece分词算法(ChatGLM、BLOOM、PaLM模型)
1.BPE(Byte Pair Encoding)分词算法是一种用于处理自然语言文本的子词分词技术,它通过统计字符对的频率来迭代地合并最常见的字符对,从而构建词汇表。这种方法能有效处理罕见词汇和新词,减少词汇表的大小,同时保留足够的信息以理解上下文。BPE广泛应用于GPT-2、BART和Llama等模型中,以提高语言生成和理解的能力。
2.WordPiece分词算法是由Google开发的一种子词分词方法,它同样是基于统计的,但与BPE不同的是,WordPiece在构建词汇表时会考虑整个语料库的似然概率,选择能够最大化似然概率的字符对进行合并。BERT模型就使用了WordPiece分词算法,这使得BERT能够有效地处理各种NLP任务,如文本分类、问答和命名实体识别等。
3.SentencePiece是另一种子词分词工具,它不仅支持BPE算法,还支持unigram语言模型。SentencePiece的主要特点是它允许直接从原始句子进行训练,不需要预定义的词汇表,这使得它在处理新词和罕见词时更为灵活。ChatGLM、BLOOM和PaLM等模型采用了SentencePiece分词算法,以增强其对话生成和理解的能力。
3.Seq2Seq结构模型
Seq2Seq(Sequence to Sequence),即序列到序列模型,是一种处理序列数据的模型框架,广泛应用于自然语言处理(NLP)领域中的各种任务,如机器翻译、文本摘要、问答系统等。Seq2Seq模型的核心思想是将一个序列转换为另一个序列,通常包括两个主要部分:编码器(Encoder)和解码器(Decoder)。
编码器(Encoder)
编码器通常是一个循环神经网络(RNN)或其变体(如LSTM或GRU),或者是更现代的Transformer架构。它的任务是读取输入序列(如一句话或一段文本),并将其编码成一个固定大小的内部表示,这个表示通常被称为上下文向量(Context Vector)或编码向量。这个向量旨在捕捉输入序列的主要信息。
解码器(Decoder)
解码器也是一个循环神经网络或Transformer架构,它使用编码器的输出作为初始状态,并逐步生成输出序列。在每个时间步,解码器会生成一个输出单元(如一个词或字符),并将其作为下一个时间步的输入,直到生成一个特殊的结束符号,表示序列结束。
工作流程
- 输入处理:输入序列首先被分词(Tokenized)和编码(如转换为词向量)。
- 编码阶段:编码器读取输入序列,并生成一个上下文向量。
- 解码阶段:解码器使用上下文向量作为初始状态,并逐步生成输出序列,直到生成结束符号。
- 输出处理:生成的序列通常需要被解码或转换回可读的文本形式。
4.加入注意力机制的Seq2Seq模型
注意力机制(Attention Mechanism)是Seq2Seq模型的一个重要改进,它允许解码器在生成每个输出元素时“关注”输入序列中的不同部分。这种机制通过计算输入序列中每个部分与当前输出的相关性权重来实现,从而使得模型能够更加关注与当前生成任务最相关的输入部分。
注意力机制的引入显著提高了Seq2Seq模型处理长序列和捕捉复杂依赖关系的能力,尤其是在机器翻译等任务中,它使得模型能够更好地理解输入序列的全局上下文,从而生成更加准确和流畅的输出。
5.日期转换实战完整代码(加入注意力机制的Seq2Seq结构模型)
import torch as th
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
import numpy as np
import random
import datetime
# Constants
SOS_token = 0
EOS_token = 1
PAD_token = 2
MAX_LENGTH = 11
# EncoderRNN
class EncoderRNN(nn.Module):
def __init__(self, input_size, hidden_size, dropout_p=0.1):
super(EncoderRNN, self).__init__()
self.hidden_size = hidden_size
self.embedding = nn.Embedding(input_size, hidden_size)
self.rnn = nn.RNN(hidden_size, hidden_size, batch_first=True)
self.dropout = nn.Dropout(dropout_p)
def forward(self, x):
x = self.embedding(x)
x = self.dropout(x)
output, hidden = self.rnn(x)
return output, hidden
# DecoderRNN
class DecoderRNN(nn.Module):
def __init__(self, hidden_size, output_size):
super(DecoderRNN, self).__init__()
self.embedding = nn.Embedding(output_size, hidden_size)
self.rnn = nn.RNN(hidden_size, hidden_size, batch_first=True)
self.out = nn.Linear(hidden_size, output_size)
def forward(self, encoder_outputs, encoder_hidden, target_tensor=None):
batch_size = encoder_outputs.size(0)
decoder_input = th.empty(batch_size, 1, dtype=th.long).fill_(SOS_token)
decoder_hidden = encoder_hidden
decoder_outputs = []
for i in range(MAX_LENGTH):
decoder_output, decoder_hidden = self.forward_step(decoder_input, decoder_hidden)
decoder_outputs.append(decoder_output)
if target_tensor is not None:
decoder_input = target_tensor[:, i].unsqueeze(1)
else:
_, topi = decoder_output.topk(1)
decoder_input = topi.squeeze(-1).detach()
decoder_outputs = th.cat(decoder_outputs, dim=1)
decoder_outputs = F.log_softmax(decoder_outputs, dim=-1)
return decoder_outputs, decoder_hidden
def forward_step(self, x, hidden):
x = self.embedding(x)
x = F.relu(x)
x, hidden = self.rnn(x, hidden)
output = self.out(x)
return output, hidden
# Attention
class Attention(nn.Module):
def __init__(self, hidden_size):
super(Attention, self).__init__()
self.Wa = nn.Linear(hidden_size, hidden_size)
self.Ua = nn.Linear(hidden_size, hidden_size)
self.Va = nn.Linear(hidden_size, 1)
def forward(self, query, keys):
scores = self.Va(th.tanh(self.Wa(query) + self.Ua(keys)))
scores = scores.squeeze(2).unsqueeze(1)
weights = F.softmax(scores, dim=-1)
context = th.bmm(weights, keys)
return context, weights
# AttentionDecoderRNN
class AttentionDecoderRNN(nn.Module):
def __init__(self, hidden_size, output_size, dropout_p=0.1):
super(AttentionDecoderRNN, self).__init__()
self.embedding = nn.Embedding(output_size, hidden_size)
self.attention = Attention(hidden_size)
self.rnn = nn.RNN(2 * hidden_size, hidden_size, batch_first=True)
self.out = nn.Linear(hidden_size, output_size)
self.dropout = nn.Dropout(dropout_p)
def forward(self, encoder_outputs, encoder_hidden, target_tensor=None):
batch_size = encoder_outputs.size(0)
decoder_input = th.empty(batch_size, 1, dtype=th.long).fill_(SOS_token)
decoder_hidden = encoder_hidden
decoder_outputs = []
attentions = []
for i in range(MAX_LENGTH):
decoder_output, decoder_hidden, attn_weights = self.forward_step(decoder_input, decoder_hidden, encoder_outputs)
decoder_outputs.append(decoder_output)
attentions.append(attn_weights)
if target_tensor is not None:
decoder_input = target_tensor[:, i].unsqueeze(1)
else:
_, topi = decoder_output.topk(1)
decoder_input = topi.squeeze(-1).detach()
decoder_outputs = th.cat(decoder_outputs, dim=1)
decoder_outputs = F.log_softmax(decoder_outputs, dim=-1)
attentions = th.cat(attentions, dim=1)
return decoder_outputs, decoder_hidden, attentions
def forward_step(self, input, hidden, encoder_outputs):
embedded = self.dropout(self.embedding(input))
query = hidden.permute(1, 0, 2)
context, attn_weights = self.attention(query, encoder_outputs)
input_rnn = th.cat((embedded, context), dim=2)
output, hidden = self.rnn(input_rnn, hidden)
output = self.out(output)
return output, hidden, attn_weights
# DateDataset
class DateDataset(Dataset):
def __init__(self, n):
self.date_cn = []
self.date_en = []
for _ in range(n):
year = random.randint(1950, 2050)
month = random.randint(1, 12)
day = random.randint(1, 28)
date = datetime.date(year, month, day)
self.date_cn.append(date.strftime("%y-%m-%d"))
self.date_en.append(date.strftime("%d/%b/%Y"))
self.vocab = set([str(i) for i in range(0, 10)] + ["-", "/"] + [i.split("/")[1] for i in self.date_en])
self.word2index = {v: i for i, v in enumerate(sorted(list(self.vocab)), start=2)}
self.word2index["<SOS>"] = SOS_token
self.word2index["<EOS>"] = EOS_token
self.word2index["<PAD>"] = PAD_token
self.vocab.add("<SOS>")
self.vocab.add("<EOS>")
self.vocab.add("<PAD>")
self.index2word = {i: v for v, i in self.word2index.items()}
self.input, self.target = [], []
for cn, en in zip(self.date_cn, self.date_en):
self.input.append([self.word2index[v] for v in cn])
self.target.append(
[self.word2index["<SOS>"], ] +
[self.word2index[v] for v in en[:3]] +
[self.word2index[en[3:6]]] +
[self.word2index[v] for v in en[6:]] +
[self.word2index["<EOS>"], ]
)
self.input, self.target = np.array(self.input), np.array(self.target)
def __len__(self):
return len(self.input)
def __getitem__(self, index):
return self.input[index], self.target[index], len(self.target[index])
@property
def num_word(self):
return len(self.vocab)
# Training
n_epochs = 100
batch_size = 32
hidden_size = 128
learning_rate = 0.001
dataset = DateDataset(1000) # Example size
dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True, drop_last=True)
encoder = EncoderRNN(dataset.num_word, hidden_size)
decoder = AttentionDecoderRNN(hidden_size, dataset.num_word)
encoder_optimizer = optim.Adam(encoder.parameters(), lr=learning_rate)
decoder_optimizer = optim.Adam(decoder.parameters(), lr=learning_rate)
criterion = nn.NLLLoss()
for i in range(n_epochs + 1):
total_loss = 0
for input_tensor, target_tensor, target_length in dataloader:
encoder_optimizer.zero_grad()
decoder_optimizer.zero_grad()
encoder_outputs, encoder_hidden = encoder(input_tensor)
decoder_outputs, _, _ = decoder(encoder_outputs, encoder_hidden, target_tensor)
loss = criterion(
decoder_outputs.view(-1, decoder_outputs.size(-1)),
target_tensor.view(-1).long()
)
loss.backward()
encoder_optimizer.step()
decoder_optimizer.step()
total_loss += loss.item()
total_loss /= len(dataloader)
if i % 10 == 0:
print(f"epoch: {i}, loss: {total_loss}")
# Evaluation
def evaluate(encoder, decoder, x):
encoder.eval()
decoder.eval()
# Ensure input is in tensor form and matches batch size
x = th.tensor(np.array([x])) # Convert input to a batch of size 1
encoder_outputs, encoder_hidden = encoder(x)
# Correctly initialize the decoder input for a batch size of 1
decoder_input = th.ones(x.shape[0], 1).long().fill_(SOS_token) # [batch_size, 1]
decoder_hidden = encoder_hidden
decoder_outputs = []
# Generate output step-by-step
for _ in range(MAX_LENGTH):
decoder_output, decoder_hidden, _ = decoder.forward_step(decoder_input, decoder_hidden, encoder_outputs)
decoder_outputs.append(decoder_output)
# Use the model's own output as the next input
_, topi = decoder_output.topk(1)
decoder_input = topi.squeeze(-1).detach()
decoder_outputs = th.cat(decoder_outputs, dim=1)
_, topi = decoder_outputs.topk(1)
# Decode the output words
decoded_ids = topi.squeeze()
decoded_words = []
for idx in decoded_ids:
decoded_words.append(dataset.index2word[idx.item()])
return ''.join(decoded_words)
for i in range(5):
predict = evaluate(encoder, decoder, dataset[i][0])
print(f"input: {dataset.date_cn[i]}, target: {dataset.date_en[i]}, predict: {predict}")
输出结果
epoch: 0, loss: 1.974310448092799
epoch: 10, loss: 0.010178287904108725
epoch: 20, loss: 0.002612524500657474
epoch: 30, loss: 0.001208579239074982
epoch: 40, loss: 0.0007198411941287979
epoch: 50, loss: 0.0022603232458594347
epoch: 60, loss: 0.00045849111460660014
epoch: 70, loss: 0.0002735930226457816
epoch: 80, loss: 0.00019530811668148324
epoch: 90, loss: 0.00014774623512846206
epoch: 100, loss: 0.00011550318477900638
input: 85-08-18, target: 18/Aug/1985, predict: 18/Aug/1985
input: 23-04-25, target: 25/Apr/2023, predict: 25/Apr/2023
input: 19-07-27, target: 27/Jul/2019, predict: 27/Jul/2019
input: 97-06-11, target: 11/Jun/1997, predict: 11/Jun/1997
input: 66-06-16, target: 16/Jun/1966, predict: 16/Jun/1966