《DeepSeek大模型高性能核心技術與多模態融合開發(人工智能技術叢書)》(王曉華)【摘要 書評 試讀】- 京東圖書
我們使用新架構的模型完成情感分類,可以看到,使用注意力機制可以很好地對特征進行抽取從而完成二分類的情感分類任務。
然而使用自注意力的模型并不僅限于此,除了經典的分類任務外,我們還可以使用增加了旋轉位置編碼RoPE的模型來完成文本生成。
4.3.1? 數據集的準備與讀取
在上一節中,我們已經完成了情感數據集的基本讀取,并掌握了漢字文本內容編碼的方法。對于本節的任務,即基于自回歸模型的酒店評論生成,數據集的準備與讀取代碼如下:
import numpy as np
from tqdm import tqdm
import torchimport tokenizer
tokenizer_emo = tokenizer.Tokenizer(model_path="../vocab/my_model.model")print(tokenizer_emo.vocab_size())
max_length = 48token_list = []
with open("../../dataset/ChnSentiCorp.txt", mode="r", encoding="UTF-8") as emotion_file:for line in tqdm(emotion_file.readlines()):line = line.strip().split(",")text = "".join(line[1:]) + '※'if True:token = tokenizer_emo.encode(text)for id in token:token_list.append(id)
token_list = torch.tensor(token_list * 2)class TextSamplerDataset(torch.utils.data.Dataset):def __init__(self, data = token_list, seq_len = max_length):super().__init__()self.data = dataself.seq_len = seq_lendef __getitem__(self, index):rand_start = torch.randint(0, self.data.size(0) - self.seq_len, (1,))full_seq = self.data[rand_start : rand_start + self.seq_len + 1].long()return full_seq[:-1],full_seq[1:]def __len__(self):return self.data.size(0) // self.seq_len
這里我們首先對文本內容進行讀取,需要注意的是,對于每行的文本內容,我們不像上一節進行情感判定時每行作為一份準備進行存儲,而是使用了首位相連的形式進行添加。這樣做的目的符合我們文本生成任務的訓練形式,即只需要按格式生成文本內容,而無需去了解所代表的含義。
TextSamplerDataset進行采樣時我們也是隨機截取了特定一段的文本進行輸出,因為隨機截取可以在一定程度上增加文本的多樣性,增強了模型的健壯性。
4.3.2? 基于自回歸文本生成模型的設計
接下來,我們需要實現基于自回歸文本生成模型的設計。首先是基本的模型設計,相對于上一節完成的分類模型,自回歸文本生成模型由于是生成任務,需要額外地添加位置編碼,即我們在本章第一節講解的旋轉位置編碼。添加了旋轉位置編碼的注意力模型如下所示。
class MultiHeadAttention_MHA(torch.nn.Module):def __init__(self, d_model, attention_head_num):super(MultiHeadAttention_MHA, self).__init__()self.attention_head_num = attention_head_numself.d_model = d_modelassert d_model % attention_head_num == 0self.scale = d_model ** -0.5self.softcap_value = 50.self.per_head_dmodel = d_model // attention_head_numself.qkv_layer = torch.nn.Linear(d_model, 3 * d_model)self.rotary_embedding = RotaryEmbedding(self.per_head_dmodel // 2, use_xpos=True)self.out_layer = torch.nn.Linear(d_model, d_model)def forward(self, embedding, past_length = 0):qky_x = self.qkv_layer(embedding)q, k, v = torch.split(qky_x, split_size_or_sections=self.d_model, dim=-1)q = einops.rearrange(q, "b s (h d) -> b h s d", h=self.attention_head_num)k = einops.rearrange(k, "b s (h d) -> b h s d", h=self.attention_head_num)v = einops.rearrange(v, "b s (h d) -> b h s d", h=self.attention_head_num)q, k = self.rotary_embedding.rotate_queries_and_keys(q, k, seq_dim=2)q = q * self.scalesim = einops.einsum(q, k, 'b h i d, b h j d -> b h i j')#sim = softclamp(sim, self.softcap_value)mask_value = -torch.finfo(sim.dtype).maxi, j = sim.shape[-2:]causal_mask = torch.ones((i, j), dtype=torch.bool).triu(past_length).to(embedding.device)sim = sim.masked_fill(causal_mask, mask_value)attn = sim.softmax(dim=-1)out = einops.einsum(attn, v, 'b h i j, b h j d -> b h i d')embedding = einops.rearrange(out, "b h s d -> b s (h d)")embedding = self.out_layer(embedding)return embedding
其中的參數past_length=0的作用是對因果掩碼進行設計,在這里triu(past_length)函數的作用是是從past_length的位置開始,生成一個對三角矩陣,對這個不理解的讀者可以嘗試如下函數:
causal_mask = torch.ones((5, 5), dtype=torch.bool).triu(0)
causal_mask = torch.ones((5, 5), dtype=torch.bool).triu(2)
打印結果并比較其中內容。
而基于標準注意力層完成的自回歸模型如下所示。
from torch import Tensor
import torch, math, einops
from moudle import attention_moudle,feedforward_layerclass EncoderBlock(torch.nn.Module):def __init__(self, d_model, num_heads):super(EncoderBlock, self).__init__()self.d_model = d_modelself.num_heads = num_headsself.attention_norm = torch.nn.RMSNorm(d_model)self.self_attention = attention_moudle.MultiHeadAttention(d_model, num_heads)self.ffn = feedforward_layer.Swiglu(d_model)def forward(self, embedding):residual = embeddingembedding = self.attention_norm(embedding)embedding = self.self_attention(embedding)embedding = self.ffn(embedding)return embedding + residualclass Encoder(torch.nn.Module):def __init__(self, d_model, num_heads, num_layers = 3):super(Encoder, self).__init__()self.layers = torch.nn.ModuleList([EncoderBlock(d_model, num_heads) for _ in range(num_layers)])def forward(self, embedding):for layer in self.layers:embedding = layer(embedding)return embeddingclass GeneratorModule(torch.nn.Module):def __init__(self, d_model, num_heads,vocab_size = 3120):super(GeneratorModule, self).__init__()self.embedding_layer = torch.nn.Embedding(vocab_size, d_model)self.encoder = Encoder(d_model, num_heads)self.logits = torch.nn.Linear(d_model, vocab_size)def forward(self, x):embedding = self.embedding_layer(x)embedding = self.encoder(embedding)logits = self.logits(embedding)return logits
同樣地,我們在模型設計中,使用了Block模塊化的設計思想完成模型的設計,通過堆疊多個模塊,對文本的特征進行抽取,并在logits層轉換后輸出。
另外還需要注意的是,由于我們目標是完成自回歸生成任務,而在輸出時則需要一個專門的輸出格式的函數對其進行輸出,代碼如下所示。
@torch.no_grad()def generate(self, prompt=None, n_tokens_to_gen=20, temperature=1., top_k=3, sample=False, eos_token=2, device="cuda"):"""根據給定的提示(prompt)生成一段指定長度的序列。參數:- seq_len: 生成序列的總長度。- prompt: 序列生成的起始提示,可以是一個列表。- temperature: 控制生成序列的隨機性。溫度值越高,生成的序列越隨機;溫度值越低,生成的序列越確定。- eos_token: 序列結束標記的token ID,默認為2。- return_seq_without_prompt: 是否在返回的序列中不包含初始的提示部分,默認為True。返回:- 生成的序列(包含或不包含初始提示部分,取決于return_seq_without_prompt參數的設置)。"""# 將輸入的prompt轉換為torch張量,并確保它在正確的設備上(如GPU或CPU)。]self.eval()# prompt = torch.tensor(prompt)prompt = prompt.clone().detach().requires_grad_(False).to(device)input_ids = promptfor token_n in range(n_tokens_to_gen):with torch.no_grad():indices_to_input = input_idsnext_token_logits = self.forward(indices_to_input)next_token_logits = next_token_logits[:, -1]probs = torch.nn.softmax(next_token_logits, dim=-1) * temperature(batch, vocab_size) = probs.shapeif top_k is not None:(values, indices) = torch.topk(probs, k=top_k)probs[probs < values[:, -1, None]] = 0probs = probs / probs.sum(axis=1, keepdims=True)if sample:next_indices = torch.multinomial(probs, num_samples=1)else:next_indices = torch.argmax(probs, dim=-1)[:, None]input_ids = torch.cat([input_ids, next_indices], dim=1)return input_ids
在上面代碼中,我們按照自回歸模型的性質,每次根據傳入的文本,生成下一個字符,之后我們將獲取的字符進行轉換,再拼接到原始文本中,這樣依次拼接的結果即獲取到完整的生成內容。
4.3.3? 評論生成模型模型的訓練
下面就是情感評論生成模型的訓練,在這里我們可以仿照前面章節訓練的過程,直接對模型進行訓練,代碼如下所示。
import math
from tqdm import tqdm
import torch
from torch.utils.data import DataLoader
import modeldevice = "cuda"
model = model.GeneratorModule(d_model=312,num_heads=6)
model.to(device)
save_path = "./saver/generator_model.pth"
model.load_state_dict(torch.load(save_path),strict=False)BATCH_SIZE = 360
seq_len = 48
import get_data_emotiontrain_dataset = get_data_emotion.TextSamplerDataset(get_data_emotion.token_list,seq_len=seq_len)
train_loader = (DataLoader(train_dataset, batch_size=BATCH_SIZE,shuffle=True))optimizer = torch.optim.AdamW(model.parameters(), lr = 2e-4)
lr_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer,T_max = 1200,eta_min=2e-5,last_epoch=-1)
criterion = torch.nn.CrossEntropyLoss()for epoch in range(60):pbar = tqdm(train_loader,total=len(train_loader))for token_inp,token_tgt in pbar:token_inp = token_inp.to(device)token_tgt = token_tgt.to(device)logits = model(token_inp)loss = criterion(logits.view(-1, logits.size(-1)), token_tgt.view(-1))optimizer.zero_grad()loss.backward()optimizer.step()lr_scheduler.step() # 執行優化器pbar.set_description(f"epoch:{epoch +1}, train_loss:{loss.item():.5f}, lr:{lr_scheduler.get_last_lr()[0]*1000:.5f}")torch.save(model.state_dict(), save_path)
讀者可以自行嘗試運行代碼查看結果。
4.3.4? 使用訓練好的模型生成評論
接下來,我們需要使用訓練好的模型生成對應的評論。根據自回歸模型的指引,我們只需要設置一個起始內容,即可根據需要生成特定的文本。代碼如下:
import torch
import modelimport tokenizer
tokenizer_emo = tokenizer.Tokenizer(model_path="../vocab/my_model.model")device = "cuda"
model = model.GeneratorModule(d_model=384,num_heads=6)
model.to(device)save_path = "./saver/generator_model.pth"
model.load_state_dict(torch.load(save_path),strict=False)model.to(device)
model.eval()for _ in range(10):text = "位置"prompt_token = tokenizer_emo.encode(text)prompt_token = torch.tensor([prompt_token]).long().to(device)result_token = model.generate(prompt=prompt_token, n_tokens_to_gen=64,top_k=5,temperature=0.99,device=device)[0].cpu().numpy()text = tokenizer_emo.decode(result_token).split("※")[0]print(text)
部分生成結果如下所示。
位置很好,就在火車站對面,離火車站很近的了,交通很便利,從機場來講是到市中心,去機場的機場,可以坐在車上。
位置不錯,在市中心,交通方便,房間也很大,服務也很好,就是房間的隔音效果比較差,而且很多人性化的服務,服務也不錯,總的來說很滿意
位置很不錯,離火車站很近也很方便。房間裝修很好,就是電視太小。
位置很好,就在繁華的路邊,出門走5分鐘就可以到了。總體來說還可以。
位置很好,就在火車站附近,步行到中心也很方便。房間也很干凈,就是有一點點小,不過還是挺干凈的,服務員也很有禮貌,有機會會來度假區住這樣的酒店很
讀者可以自行嘗試學習。