基于自回归模型的酒店评论生成

《DeepSeek大模型高性能核心技术与多模态融合开发(人工智能技术丛书)》(王晓华)【摘要 书评 试读】- 京东图书

我们使用新架构的模型完成情感分类,可以看到,使用注意力机制可以很好地对特征进行抽取从而完成二分类的情感分类任务。

然而使用自注意力的模型并不仅限于此,除了经典的分类任务外,我们还可以使用增加了旋转位置编码RoPE的模型来完成文本生成。

4.3.1  数据集的准备与读取

在上一节中,我们已经完成了情感数据集的基本读取,并掌握了汉字文本内容编码的方法。对于本节的任务,即基于自回归模型的酒店评论生成,数据集的准备与读取代码如下:

import numpy as np
from tqdm import tqdm
import torch

import tokenizer
tokenizer_emo = tokenizer.Tokenizer(model_path="../vocab/my_model.model")

print(tokenizer_emo.vocab_size())
max_length = 48

token_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 = data
        self.seq_len = seq_len

    def __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_num
        self.d_model = d_model

        assert d_model % attention_head_num == 0
        self.scale = d_model ** -0.5
        self.softcap_value = 50.
        self.per_head_dmodel = d_model // attention_head_num

        self.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.scale
        sim = 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).max

        i, 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_layer

class EncoderBlock(torch.nn.Module):
    def __init__(self, d_model, num_heads):
        super(EncoderBlock, self).__init__()
        self.d_model = d_model
        self.num_heads = num_heads
        self.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 = embedding

        embedding = self.attention_norm(embedding)
        embedding = self.self_attention(embedding)
        embedding = self.ffn(embedding)

        return embedding + residual

class 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 embedding

class 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 = prompt
        for token_n in range(n_tokens_to_gen):
            with torch.no_grad():
                indices_to_input = input_ids
                next_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.shape

            if top_k is not None:
                (values, indices) = torch.topk(probs, k=top_k)
                probs[probs < values[:, -1, None]] = 0
                probs = 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 model

device = "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_emotion

train_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 model

import 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分钟就可以到了。总体来说还可以。
位置很好,就在火车站附近,步行到中心也很方便。房间也很干净,就是有一点点小,不过还是挺干净的,服务员也很有礼貌,有机会会来度假区住这样的酒店很

读者可以自行尝试学习。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值