当前位置: 首页 > news >正文

【循环神经网络】案例:周杰伦歌词文本预测【训练+python代码】

文章目录

  • 1、简介
  • 2、数据集
  • 3、构建词典
  • 4、构建数据集对象
  • 5、构建网络模型
  • 6、构建训练函数
    • 6.1、多分类交叉熵损失函数🔺
    • 6.2、Adam🔺
    • 6.3、代码
  • 7、构建预测函数
  • 8、word_to_index和index_to_word
    • 8.1、word_to_index
    • 8.2、index_to_word
    • 8.3、使用场景
  • 9、DataLoader()的参数
  • 10、标准深度学习模型训练流程⭐
  • 11、案例的完整代码

🍃作者介绍:双非本科大三网络工程专业在读,阿里云专家博主,专注于Java领域学习,擅长web应用开发、数据结构和算法,初步涉猎人工智能和前端开发。
🦅个人主页:@逐梦苍穹
📕所属专栏:人工智能
🌻gitee地址:xzl的人工智能代码仓库
✈ 您的一键三连,是我创作的最大动力🌹

1、简介

本文目标:掌握文本生成模型构建流程

文本生成任务是一种常见的自然语言处理任务,输入一个开始词能够预测出后面的词序列。
本案例将会使用循环神经网络来实现周杰伦歌词生成预测任务。

2、数据集

免费下载
image.png
链接:https://pan.baidu.com/s/14SJWD_ChjiX0UJ8ncL6a4w?pwd=1234
提取码:1234

数据集如下:
image.png
数据集共有 5819 行。
下面是讲解原理之后,用代码依次构建,文末会有完整代码文本。

3、构建词典

我们在进行自然语言处理任务之前,首要做的就是就是构建词表。
所谓的词表就是将 语料进行分词,然后给每一个词分配一个唯一的编号,便于我们送入词嵌入层
最终,我们的词典主要包含了:

  1. word_to_index:存储了词到编号的映射
  2. index_to_word:存储了编号到词的映射

(下文讲解 word_to_indexindex_to_word 的区别和用处)

一般构建词表的流程如下:

  1. 语料清洗,去除不相关的内容
  2. 对语料进行分词
  3. 构建词表

接下来,我们对周杰伦歌词的语料数据按照上面的步骤构建词表。
代码如下:

# -*- coding: utf-8 -*-
# @Author: CSDN@逐梦苍穹
# @Time: 2024/8/7 16:34# 构建词典
import re  # 导入正则表达式模块,用于文本清洗和处理import jieba  # 导入jieba库,用于中文分词# 构建词汇表
def build_vocab():file_name = '../data-txt/jaychou_lyrics.txt'# TODO 1.清洗文本# 初始化一个列表,用于存储清洗后的句子clean_sentences = []for line in open(file_name, 'r', encoding='utf-8'):  # 打开歌词文件,逐行读取,指定编码为UTF-8line = line.replace('〖韩语Rap译文〗', '')  # 去除特定的文本标记# TODO 使用正则表达式去除无效字符# 去除 除了 中文、英文、数字、部分标点符号外 的其他字符line = re.sub(r'[^\u4e00-\u9fa5 a-zA-Z0-9!?,]', '', line)# 连续空格替换成1个line = re.sub(r'[ ]{2,}', '', line)  # 替换连续的空格为单个空格# 去除两侧空格、换行line = line.strip()  # 去除行首和行尾的空白字符和换行符if len(line) <= 1:  # 如果行的长度小于等于1,则跳过该行continue# 去除重复行if line not in clean_sentences:  # 如果行不在clean_sentences中,则添加clean_sentences.append(line)  # 将清洗后的行添加到clean_sentences列表中# TODO 2. 语料分词# TODO 初始化两个列表: index_to_word用于存储词汇表, all_sentences用于存储所有分词后的句子index_to_word, all_sentences = [], []for line in clean_sentences:  # 遍历清洗后的句子words = jieba.lcut(line)  # 使用jieba对句子进行分词,返回词汇列表# print("words: ", words)all_sentences.append(words)  # 将分词结果添加到all_sentences列表中# print("all_sentences: ", all_sentences)for word in words:  # 遍历每个词if word not in index_to_word:  # 如果词不在词汇表中# print("word: ", word)index_to_word.append(word)  # 将词添加到词汇表中# print("index_to_word: ", index_to_word)# 词到索引映射word_to_index = {word: idx for idx, word in enumerate(index_to_word)}  # 创建词到索引的映射字典# print("word_to_index_start: ", word_to_index)# 词的数量word_count = len(index_to_word)  # 计算词汇表中词的数量# 句子索引表示corpus_idx = []  # 初始化一个列表,用于存储整个语料的索引表示for sentence in all_sentences:  # 遍历每个分词后的句子temp = []  # 初始化一个临时列表,用于存储句子的索引for word in sentence:  # 遍历句子中的每个词temp.append(word_to_index[word])  # 将词转换为索引并添加到临时列表中# 在每行歌词之间添加空格隔开temp.append(word_to_index[' '])  # 在每个句子末尾添加空格的索引作为分隔符# print("temp: ", temp)# TODO extend()是逐个添加, 区别于extend()corpus_idx.extend(temp)  # 将句子的索引表示添加到corpus_idx列表中# print("corpus_idx: ", corpus_idx)# TODO 返回构建的词汇表、索引映射、词数、语料索引return index_to_word, word_to_index, word_count, corpus_idxif __name__ == '__main__':  # 主程序入口index_to_word, word_to_index, word_count, corpus_idx = build_vocab()  # 调用build_vocab函数,构建词汇表和索引print("词汇表的大小: ", word_count)  # 打印词汇表的大小print("语料的索引表示: ", corpus_idx[:10])  # 打印语料的索引表示print("索引到词的映射: ", index_to_word[:10])  # 打印词汇表print("词到索引的映射: ", word_to_index)  # 打印词到索引的映射

输出如下:
image.png
一些代码概念的理解:
image.png
image.png
对代码具体实现流程的总结如下:

  1. 清洗文本:去除特定文本、无效字符、连续空格转为单个空格、去掉换行符和两侧空格
  2. 分词:构建"index_to_word"->“索引 : 词”;“all_sentences”->分词后的句子
    1. 对每行进行分词,加如all_sentences列表
    2. 对每个词索引映射:index_to_word
  3. 构建"word_to_index"->“词 : 索引”
  4. 定义corpus_idx语料集合,存储所有分词后的文本索引(通过添加末尾空格[单个]索引,来辨认哪些索引对应某一句歌词)
  5. 返回index_to_word, word_to_index, word_count, corpus_idx

4、构建数据集对象

我们在训练的时候,为了便于读取语料,并送入网络,所以我们会构建一个 Dataset 对象,并使用该对象构建 DataLoader 对象,然后对 DataLoader 对象进行迭代可以获取语料,并将其送入网络
类对象:
image.png
使用:
image.png
输出:

E:\anaconda3\python.exe D:\Python\AI\神经网络\18-案例:周杰伦歌词文本预测\分部测试\2-构建数据集.py 
Building prefix dict from the default dictionary ...
Loading model from cache C:\Users\86189\AppData\Local\Temp\jieba.cache
Loading model cost 0.796 seconds.
Prefix dict has been built successfully.
lyrics:  <__main__.LyricsDataset object at 0x000001ACA162BE90>
x: tensor([[ 0,  1,  2, 39,  0]])
y: tensor([[ 1,  2, 39,  0,  3]])Process finished with exit code 0

对代码具体实现流程的总结如下:

  1. 先由前文的buid_vocab()获得对应的单个词典
  2. 把该词典的语料集和想要划分每批次的数值,输入到类中
  3. 初始化各数值(init
  4. 构造样本,构建开始索引。有x输入值和y输出值,转换为tensor张量,为下文的模型构造和训练做准备

5、构建网络模型

我们用于实现歌词生成的网络模型,主要包含了三个层:

  1. 词嵌入层: 用于将语料转换为词向量
  2. 循环网络层:提取句子语义
  3. 全连接层:输出对词典中每个词的预测概率

我们前面学习了 Dropout 层,它具有正则化作用,所以在我们的网络层中,我们会对词嵌入层、循环网络层的输出结果进行 Dropout 计算。

代码:

# 定义一个文本生成模型类,继承自 nn.Module
class TextGenerator(nn.Module):# 初始化方法,构造函数def __init__(self, vocab_size):print("vocab_size: ", vocab_size)# 调用父类(nn.Module)的构造函数super(TextGenerator, self).__init__()# TODO 定义新变量ebd、rnn、out# 初始化词嵌入层,将词汇表中的每个词映射到一个128维的向量self.ebd = nn.Embedding(vocab_size, 128)# 初始化循环神经网络层,输入和输出都是128维,层数为1self.rnn = nn.RNN(128, 128, 1)# 初始化线性输出层,将RNN的输出映射到词汇表的大小# TODO 输出是需要转为词汇, 所以vocab_size多大, 输出就是多大的self.out = nn.Linear(128, vocab_size)# 定义前向传播方法def forward(self, inputs, hidden):# 将输入的词索引转换为嵌入向量,输出维度为 (1, 5, 128)embed = self.ebd(inputs)# 对嵌入向量进行dropout正则化,防止过拟合,概率为0.2embed = F.dropout(embed, p=0.2)# 将嵌入向量的维度从 (1, 5, 128) 转置为 (5, 1, 128) 以匹配RNN的输入要求# TODO 这里会调用rnn.py的forward()方法output, hidden = self.rnn(embed.transpose(0, 1), hidden)  # embed.transpose(0, 1)调整张量维度# 对RNN的输出进行dropout正则化,防止过拟合,概率为0.2embed = F.dropout(output, p=0.2)# 将RNN的输出维度从 (5, 1, 128) 压缩为 (5, 128)# 然后通过线性层将其转换为词汇表大小的向量 (5, vocab_size)output = self.out(output.squeeze())# 返回输出和隐藏状态return output, hidden# 初始化隐藏状态的方法def init_hidden(self):# 返回一个全零的隐藏状态张量,维度为 (1, 1, 128)return torch.zeros(1, 1, 128)

测试:
image.png
输出:

E:\anaconda3\python.exe D:\Python\AI\神经网络\18-案例:周杰伦歌词文本预测\分部测试\3-构建网络模型.py 
Building prefix dict from the default dictionary ...
Loading model from cache C:\Users\86189\AppData\Local\Temp\jieba.cache
Loading model cost 0.764 seconds.
Prefix dict has been built successfully.
词汇表的大小:  5682
语料的索引表示:  [0, 1, 2, 39, 0, 3, 4, 5, 6, 7]
索引到词的映射:  ['想要', '有', '直升机', '和', '你', '飞到', '宇宙', '去', '融化', '在']
vocab_size:  5682
torch.Size([1, 5])Process finished with exit code 0

RNN输入要求:
image.png
rnn.forward():
image.png
返回结果是一个时间步输出隐藏状态,一个输出最终的隐藏状态

6、构建训练函数

前面的准备工作完成之后,就可以编写训练函数了
训练函数主要负责编写 数据迭代、送入网络、计算损失、反向传播、更新参数,其流程基本较为固定。
由于我们要实现文本生成,文本生成本质上,输入一串文本,预测下一个文本,也属于分类问题,可以看成是一个 多分类问题
所以,我们使用 多分类交叉熵损失函数
优化方法这里选择学习率、梯度自适应的 Adam 算法作为我们的优化方法。
训练完成之后,我们使用 torch.save 方法将模型持久化存储。

6.1、多分类交叉熵损失函数🔺

多分类交叉熵损失函数(Multiclass Cross-Entropy Loss)是深度学习中常用的损失函数,特别适用于多分类任务。
它计算模型 输出的概率分布真实类别分布之间的差异
对于一个具有 N N N 个样本和 C C C 类别的多分类问题,多分类交叉熵损失函数定义为:
Loss = − 1 N ∑ i = 1 N ∑ c = 1 C y i , c log ⁡ ( y ^ i , c ) \text{Loss} = -\frac{1}{N} \sum_{i=1}^{N} \sum_{c=1}^{C} y_{i,c} \log(\hat{y}_{i,c}) Loss=N1i=1Nc=1Cyi,clog(y^i,c)
其中:

  • N N N 是样本数量。
  • C C C 是类别数量。
  • y i , c y_{i,c} yi,c 是样本 i i i 的真实标签,如果样本 i i i 的真实类别是 c c c,则 y i , c = 1 y_{i,c} = 1 yi,c=1,否则 y i , c = 0 y_{i,c} = 0 yi,c=0
  • y ^ i , c \hat{y}_{i,c} y^i,c 是模型对样本 i i i 的类别 c c c 的预测概率。

实现:

  1. 在PyTorch中,多分类交叉熵损失函数可以使用 torch.nn.CrossEntropyLoss 类来实现。
  2. 该类结合了 nn.LogSoftmaxnn.NLLLoss,因此无需手动应用 softmax 函数到模型的输出。
  3. CrossEntropyLoss 会自动计算 softmax,然后再计算交叉熵损失。

6.2、Adam🔺

Adam简介:
Adam(Adaptive Moment Estimation)优化器是 深度学习中常用的一种优化算法。
它结合了AdaGrad和RMSProp的优点,既能够适应稀疏梯度,又能够处理非平稳目标。
Adam通过计算梯度的
一阶和二阶矩估计动态调整每个参数学习率

一阶动量是梯度的指数加权移动平均。它可以看作是梯度的平均值,表示了梯度的方向和大小。
二阶动量是梯度平方的指数加权移动平均。它可以看作是梯度的方差,表示了梯度的变化范围。

Adam 优化器的关键特点:

  1. 自适应学习率:每个参数都有独立的学习率,可以根据一阶和二阶梯度估计动态调整。
  2. 计算效率相对于简单的随机梯度下降 (SGD)Adam计算效率高,存储需求小
  3. 适用于大规模数据集:在处理大规模数据集和高维参数空间时表现优越。
  4. 默认超参数效果好:在许多情况下,Adam的默认超参数配置能取得很不错的效果。

Adam通过以下公式来更新参数:

  1. 计算梯度的移动平均
    • m t = β 1 m t − 1 + ( 1 − β 1 ) g t m_t = \beta_1 m_{t-1} + (1 - \beta_1) g_t mt=β1mt1+(1β1)gt
    • v t = β 2 v t − 1 + ( 1 − β 2 ) g t 2 v_t = \beta_2 v_{t-1} + (1 - \beta_2) g_t^2 vt=β2vt1+(1β2)gt2
    • 其中, g t g_t gt 是当前时间步的梯度, m t m_t mt v t v_t vt 分别是梯度的一阶和二阶矩的移动平均, β 1 \beta_1 β1 β 2 \beta_2 β2 是衰减率(通常取 β 1 = 0.9 \beta_1 = 0.9 β1=0.9 β 2 = 0.999 \beta_2 = 0.999 β2=0.999)。
  2. 偏差修正
    • m t ^ = m t 1 − β 1 t \hat{m_t} = \frac{m_t}{1 - \beta_1^t} mt^=1β1tmt
    • v t ^ = v t 1 − β 2 t \hat{v_t} = \frac{v_t}{1 - \beta_2^t} vt^=1β2tvt
  3. 参数更新
    • θ t = θ t − 1 − α m t ^ v t ^ + ϵ \theta_t = \theta_{t-1} - \alpha \frac{\hat{m_t}}{\sqrt{\hat{v_t}} + \epsilon} θt=θt1αvt^ +ϵmt^
    • 其中, α \alpha α 是学习率, ϵ \epsilon ϵ 是一个小常数(防止分母为零,通常取 1 0 − 8 10^{-8} 108)。

6.3、代码

定义训练函数:

def train(epoch, train_log):# 构建词典,返回索引到词,词到索引,词汇数量,和语料索引列表index_to_word, word_to_index, word_count, corpus_idx = build_vocab()# 创建歌词数据集实例,每个输入序列的长度为 32lyrics = LyricsDataset(corpus_idx, 32)# 初始化文本生成模型,词汇表大小为 word_countmodel = TextGenerator(word_count)# TODO 正式进入这一阶段代码# 定义交叉熵损失函数criterion = nn.CrossEntropyLoss()# 定义 Adam 优化器,学习率为 1e-3optimizer = optim.Adam(model.parameters(), lr=1e-3)# 打开日志文件用于写入file = open(train_log, 'w')# 开始训练循环for epoch_idx in range(epoch):# 数据加载器,打乱顺序,每次取 1 个样本lyrics_dataloader = DataLoader(lyrics, shuffle=True, batch_size=1)# 记录训练开始时间start = time.time()# 重置迭代次数iter_num = 0# 重置训练损失total_loss = 0.0# 遍历数据加载器中的数据for x, y in lyrics_dataloader:# 初始化隐藏状态hidden = model.init_hidden()# 前向传播计算输出和隐藏状态output, hidden = model(x, hidden)# 计算损失,y.squeeze() 去掉维度大小为1的维度# TODO 计算模型输出与实际目标之间的损失(误差)loss = criterion(output, y.squeeze())# 梯度清零optimizer.zero_grad()# 反向传播计算梯度loss.backward()# 更新参数optimizer.step()# 迭代次数加1iter_num += 1# 累加损失total_loss += loss.item()# 构建本次迭代的日志信息message = 'epoch %3s loss: %.5f time %.2f' % \(epoch_idx + 1,  # 当前训练轮数total_loss / iter_num,  # 平均损失time.time() - start)  # 本轮训练时间# 打印日志信息print(message)# 写入日志文件file.write(message + '\n')# 关闭日志文件file.close()# 保存模型参数到文件torch.save(model.state_dict(), '../model/lyrics_model_%d.bin' % epoch)if __name__ == '__main__':train(epoch=1, train_log='lyrics_training.log')  # 设置训练轮数为 200, 训练日志文件名为 lyrics_training.log

7、构建预测函数

到了最后一步,从磁盘加载训练好的模型,进行预测
预测函数,输入第一个指定的词,我们将该词输入网路,预测出下一个词,再将预测的出的词再次送入网络,预测出下一个词,以此类推,直到预测出我们指定长度的内容。

# 构建预测函数
def predict(start_word, sentence_length, model_path):# 构建词典,返回索引到词,词到索引,词汇数量index_to_word, word_to_index, word_count, _ = build_vocab()# 构建文本生成模型实例,词汇表大小为 word_countmodel = TextGenerator(vocab_size=word_count)# 加载训练好的模型参数model.load_state_dict(torch.load(model_path))# 初始化隐藏状态hidden = model.init_hidden()try:# 将起始词转换为词索引word_idx = word_to_index[start_word]except:print("该词不在词典中, 请重新输入")return# 用于存储生成的句子(词索引序列)generate_sentence = [word_idx]# 生成长度为 sentence_length 的句子for _ in range(sentence_length):# 前向传播,获取模型输出和隐藏状态output, hidden = model(torch.tensor([[word_idx]]), hidden)# print("output: ", output)# 获取输出中概率最大的词的索引word_idx = torch.argmax(output).item()# 将该词索引添加到生成的句子中generate_sentence.append(word_idx)# 将生成的词索引序列转换为实际词并打印for idx in generate_sentence:print(index_to_word[idx], end='')print()if __name__ == '__main__':predict('分手', 15, '../model/lyrics_model_20.bin')

程序运行结果:

E:\anaconda3\python.exe D:\Python\AI\神经网络\18-案例:周杰伦歌词文本预测\分部测试\5-构建预测函数.py 
Building prefix dict from the default dictionary ...
Loading model from cache C:\Users\86189\AppData\Local\Temp\jieba.cache
Loading model cost 0.873 seconds.
Prefix dict has been built successfully.
vocab_size:  5682
分手的话像语言暴力 我已无能为力再提起 决定中断熟悉 Process finished with exit code 0

关于 torch.argmax(output):

这行代码用于从模型的输出中选择 概率 最大的词的索引,并将其作为下一个输入词的索引

那么在训练过程中,又是什么时候存储概率的呢?
在训练过程中,模型的输出并不是直接存储概率,而是生成一个未归一化的得分(logits)向量。、通过交叉熵损失函数计算时,这些未归一化的得分会被隐式地转换为概率。
1. 模型输出(Logits)
模型的最后一层通常是一个线性层,它的输出是一个包含词汇表中每个词的未归一化得分(logits)的向量。这些得分代表了每个词在当前上下文中的相对可能性。
2. 交叉熵损失函数:
在训练过程中,使用 nn.CrossEntropyLoss 作为损失函数。nn.CrossEntropyLoss 函数结合了 nn.LogSoftmaxnn.NLLLoss,其计算步骤如下:

  1. Logits to Probabilities
    • nn.LogSoftmax 将未归一化的得分(logits)转换为对数概率。
  2. Negative Log-Likelihood Loss
    • nn.NLLLoss 使用对数概率计算负对数似然损失。

3. 训练过程:
在训练过程中,模型的输出是未归一化的得分,这些得分在计算交叉熵损失时被隐式地转换为概率。训练完成后,模型在预测时会输出这些得分,通过 torch.argmax 找到得分最高的词索引,这个词就是模型预测的下一个词。

8、word_to_index和index_to_word

在自然语言处理(NLP)任务中,文本数据需要转换为数值格式,以便机器学习模型可以处理和理解。 word_to_indexindex_to_word 是两种常用的数据结构,用于将词汇表中的单词和索引相互映射。这种映射在文本预处理中非常重要,下面是对这两者的详细解释和作用。

8.1、word_to_index

word_to_index 是一个字典,它将每个单词映射到一个唯一的整数索引。这种映射的主要作用有:

  1. 文本向量化
    • 目的:将文本转换为数字形式,便于输入机器学习模型。
    • 实现:每个单词用其对应的整数索引表示,形成数值向量。
  2. 词汇表管理
    • 作用:在训练模型时,我们需要知道输入数据中有哪些单词。
    • 方便查询:通过索引,我们可以快速查找单词在词汇表中的位置。
  3. 简化计算
    • 优势:数字表示更易于机器处理,比处理字符串更高效。

8.2、index_to_word

index_to_word 是一个字典,它将每个整数索引映射回对应的单词。这种映射的主要作用有:

  1. 结果解码
    • 目的:在预测阶段,将模型输出的索引转换回可读的文本。
  2. 模型解释
    • 优势:帮助理解和分析模型的输出结果,尤其是在生成文本时。
  3. 调试和可视化
    • 作用:便于在训练和调试过程中,查看数值向量对应的实际单词,帮助检查和调整模型性能。

8.3、使用场景

  • 构建词汇表:在处理文本数据时,首先需要通过所有文本构建一个词汇表,并使用 word_to_indexindex_to_word 映射来管理词汇和索引的关系。
  • 预处理文本:在将文本数据输入模型前,使用 word_to_index 将文本转换为数值形式。
  • 解码预测:在生成模型(如文本生成或机器翻译)中,使用 index_to_word 将模型的输出转化为可读的文本。

9、DataLoader()的参数

DataLoader 是 PyTorch 中的一个核心类,用于加载数据集并提供迭代器接口来批量访问数据。
它在处理大规模数据集时尤其有用,因为它可以自动处理批量化、打乱顺序、并行加载等任务。
下面是 DataLoader 的一些常用参数及其作用的详细解释:
DataLoader 常用参数:

DataLoader(dataset, batch_size=1, shuffle=False, sampler=None, batch_sampler=None, num_workers=0, collate_fn=None, pin_memory=False, drop_last=False, timeout=0, worker_init_fn=None, multiprocessing_context=None,generator=None,prefetch_factor=2,persistent_workers=False
)
  1. dataset
  • 类型Dataset 类的实例
  • 说明:指定数据集,DataLoader 从中读取数据。数据集需要实现 __len____getitem__ 方法。
  1. batch_size
  • 类型int
  • 默认值1
  • 说明:每个批次的数据样本数量。DataLoader 会按指定的 batch_size 从数据集中提取数据。
  1. shuffle
  • 类型bool
  • 默认值False
  • 说明:如果为 True,每个epoch在提取数据时会将数据集打乱顺序。打乱顺序有助于打破数据中的顺序依赖,提高模型的泛化能力。
  1. sampler
  • 类型Sampler 类的实例
  • 说明:定义了从数据集中抽样的策略。如果设置了 sampler,则 shuffle 必须为 False
  1. batch_sampler
  • 类型Sampler 类的实例
  • 说明:与 sampler 类似,但返回的是一个批次的索引,而不是单个索引。与 batch_sizeshufflesampler 互斥。
  1. num_workers
  • 类型int
  • 默认值0
  • 说明:用于数据加载的子进程数量。设置为 0 时,数据将在主进程中加载。增加此参数的值可以加快数据加载速度,尤其是在IO密集型数据集上
  1. collate_fn
  • 类型Callable
  • 说明:用于将一个批次的数据合并为一个单一的张量。如果需要对每个批次的数据进行特殊处理,可以定义自己的 collate_fn
  1. pin_memory
  • 类型bool
  • 默认值False
  • 说明:如果设置为 TrueDataLoader 将会在返回之前,将张量复制到CUDA固定内存中,提高GPU数据加载速度
  1. drop_last
  • 类型bool
  • 默认值False
  • 说明:如果为 TrueDataLoader 会丢弃数据集中无法组成完整批次的最后几个样本。这在确保批次大小一致时很有用
  1. timeout
  • 类型float
  • 默认值0
  • 说明:设置 DataLoader 在获取一个批次的数据时的超时时间。超过这个时间将会报错
  1. worker_init_fn
  • 类型Callable
  • 说明:每个子进程启动时,会调用这个函数进行初始化。可以用于设置每个工作进程的随机种子
  1. multiprocessing_context
  • 类型multiprocessing 模块的上下文
  • 说明:指定使用的多进程上下文,例如 'spawn''fork'。在不同平台上,多进程的默认行为可能不同
  1. generator
  • 类型torch.Generator
  • 说明:控制 DataLoader 随机数生成的状态。可以通过设置随机种子,确保数据加载的一致性和可重复性
  1. prefetch_factor
  • 类型int
  • 默认值2
  • 说明:每个工作进程应预取的样本批次数。仅当 num_workers > 0 时使用
  1. persistent_workers
  • 类型bool
  • 默认值False
  • 说明:如果设置为 True,则数据加载器将在整个epoch内保持其工作进程的运行状态,而不会在每个epoch后关闭它们

10、标准深度学习模型训练流程⭐

  1. 数据准备
    • 数据加载:从文件或其他数据源加载数据。
    • 数据预处理:清洗数据,分词,构建词汇表,将数据转换为适合输入模型的格式。
    • 数据集和数据加载器:将预处理后的数据封装成数据集,并使用数据加载器批量加载数据。
  2. 模型构建
    • 模型定义:定义模型的架构,包括输入层、隐藏层、输出层等。
    • 初始化模型:创建模型实例,设置超参数(如词汇表大小、嵌入维度、隐藏层维度等)。
  3. 损失函数和优化器
    • 损失函数:选择适合任务的损失函数,如交叉熵损失用于分类问题。
    • 优化器:选择优化算法,如 Adam、SGD,并设置学习率等超参数。
  4. 训练设置
    • 训练轮数:设置训练的轮数(epoch)。
    • 批量大小:设置每次训练的批量大小(batch size)。
  5. 训练循环
    • 外层循环(epoch):遍历每一轮训练。
      • 数据加载:通过数据加载器批量加载数据。
      • 重置指标:如总损失、迭代次数。
      • 内层循环(batch):遍历每个批次数据。
        • 初始化隐藏状态:为每个批次数据初始化隐藏状态(对于 RNN 类模型)。
        • 前向传播:将输入数据通过模型,计算输出。
        • 计算损失:使用损失函数计算模型输出与目标值之间的误差。
        • 反向传播:计算梯度,进行梯度清零,执行反向传播。
        • 更新参数:通过优化器更新模型参数。
        • 记录指标:累加损失、更新迭代次数等。
      • 记录日志:每轮结束后,计算平均损失和训练时间,打印并记录日志信息。
  6. 保存模型
    • 模型保存:训练结束后,保存模型的参数到文件,以便后续使用。

11、案例的完整代码

下面奉上本次案例的完整代码。
目录结构如下(gitee地址:https://gitee.com/xzl-it/artificial-intelligence):
image.png
完整代码(含注释):

# -*- coding: utf-8 -*-
# @Author: CSDN@逐梦苍穹
# @Time: 2024/8/8 1:14
import re  # 导入正则表达式模块,用于文本清洗和处理
import time
import jieba  # 导入jieba库,用于中文分词
import torch  # 导入 PyTorch 库,用于深度学习
import torch.nn as nn  # 从 PyTorch 中导入神经网络模块
import torch.nn.functional as F  # 从 PyTorch 中导入功能模块,用于实现一些常见的神经网络操作
import torch.optim as optim  # 导入 PyTorch 的优化器模块
from torch.utils.data import DataLoader  # 从 PyTorch 中导入 DataLoader 类,用于批量数据加载# 检查是否有可用的 GPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("device: ", device)def build_vocab():file_name = 'data-txt/jaychou_lyrics.txt'# TODO 1.清洗文本# 初始化一个列表,用于存储清洗后的句子clean_sentences = []for line in open(file_name, 'r', encoding='utf-8'):  # 打开歌词文件,逐行读取,指定编码为UTF-8line = line.replace('〖韩语Rap译文〗', '')  # 去除特定的文本标记# TODO 使用正则表达式去除无效字符# 去除 除了 中文、英文、数字、部分标点符号外 的其他字符line = re.sub(r'[^\u4e00-\u9fa5 a-zA-Z0-9!?,]', '', line)# 连续空格替换成1个line = re.sub(r'[ ]{2,}', '', line)  # 替换连续的空格为单个空格# 去除两侧空格、换行line = line.strip()  # 去除行首和行尾的空白字符和换行符if len(line) <= 1:  # 如果行的长度小于等于1,则跳过该行continue# 去除重复行if line not in clean_sentences:  # 如果行不在clean_sentences中,则添加clean_sentences.append(line)  # 将清洗后的行添加到clean_sentences列表中# TODO 2. 语料分词# TODO 初始化两个列表: index_to_word用于存储词汇表, all_sentences用于存储所有分词后的句子index_to_word, all_sentences = [], []for line in clean_sentences:  # 遍历清洗后的句子words = jieba.lcut(line)  # 使用jieba对句子进行分词,返回词汇列表# print("words: ", words)all_sentences.append(words)  # 将分词结果添加到all_sentences列表中# print("all_sentences: ", all_sentences)for word in words:  # 遍历每个词if word not in index_to_word:  # 如果词不在词汇表中# print("word: ", word)index_to_word.append(word)  # 将词添加到词汇表中# print("index_to_word: ", index_to_word)# 词到索引映射word_to_index = {word: idx for idx, word in enumerate(index_to_word)}  # 创建词到索引的映射字典# print("word_to_index_start: ", word_to_index)# 词的数量word_count = len(index_to_word)  # 计算词汇表中词的数量# 句子索引表示corpus_idx = []  # 初始化一个列表,用于存储整个语料的索引表示for sentence in all_sentences:  # 遍历每个分词后的句子temp = []  # 初始化一个临时列表,用于存储句子的索引for word in sentence:  # 遍历句子中的每个词temp.append(word_to_index[word])  # 将词转换为索引并添加到临时列表中# 在每行歌词之间添加空格隔开temp.append(word_to_index[' '])  # 在每个句子末尾添加空格的索引作为分隔符# print("temp: ", temp)# TODO extend()是逐个添加, 区别于extend()corpus_idx.extend(temp)  # 将句子的索引表示添加到corpus_idx列表中# print("corpus_idx: ", corpus_idx)# TODO 返回构建的词汇表、索引映射、词数、语料索引return index_to_word, word_to_index, word_count, corpus_idx# 处理歌词数据集
class LyricsDataset:def __init__(self, corpus_idx, num_chars):  # 初始化方法,构造函数# 语料数据self.corpus_idx = corpus_idx  # 将传入的语料索引列表存储为类的属性# 语料长度self.num_chars = num_chars  # 将每个输入序列的长度(字符数)存储为类的属性# 词的数量self.word_count = len(self.corpus_idx)  # 计算语料索引列表的长度,即词汇总数# 句子数量self.number = self.word_count // self.num_chars  # 计算可以提取的样本序列的数量,每个样本长度为 num_charsdef __len__(self):  # 定义返回数据集大小的方法return self.number  # 返回可以提取的样本序列数量# TODO 后续通过DataLoader()加载这里的数据def __getitem__(self, idx):  # 定义获取数据集中某个样本的方法# 修正索引值到: [0, self.word_count - 1]start = min(max(idx, 0), self.word_count - self.num_chars - 2)  # 限制起始索引,确保有效的样本提取范围x = self.corpus_idx[start: start + self.num_chars]  # 获取从起始索引开始的 num_chars 长度的序列作为输入y = self.corpus_idx[start + 1: start + 1 + self.num_chars]  # 获取从起始索引+1开始的 num_chars 长度的序列作为目标return torch.tensor(x), torch.tensor(y)  # 返回输入和目标序列作为张量# 定义一个文本生成模型类,继承自 nn.Module
class TextGenerator(nn.Module):# 初始化方法,构造函数def __init__(self, vocab_size):print("vocab_size: ", vocab_size)# 调用父类(nn.Module)的构造函数super(TextGenerator, self).__init__()# TODO 定义新变量ebd、rnn、out# 初始化词嵌入层,将词汇表中的每个词映射到一个128维的向量self.ebd = nn.Embedding(vocab_size, 128)# 初始化循环神经网络层,输入和输出都是128维,层数为1self.rnn = nn.RNN(128, 128, 1)# 初始化线性输出层,将RNN的输出映射到词汇表的大小# TODO 输出是需要转为词汇, 所以vocab_size多大, 输出就是多大的self.out = nn.Linear(128, vocab_size)# 定义前向传播方法def forward(self, inputs, hidden):# 将输入的词索引转换为嵌入向量,输出维度为 (1, 5, 128)embed = self.ebd(inputs).to(device)# 对嵌入向量进行dropout正则化,防止过拟合,概率为0.2embed = F.dropout(embed, p=0.2)# 将嵌入向量的维度从 (1, 5, 128) 转置为 (5, 1, 128) 以匹配RNN的输入要求# TODO 这里会调用rnn.py的forward()方法output, hidden = self.rnn(embed.transpose(0, 1), hidden)  # embed.transpose(0, 1)调整张量维度# 对RNN的输出进行dropout正则化,防止过拟合,概率为0.2embed = F.dropout(output, p=0.2)# 将RNN的输出维度从 (5, 1, 128) 压缩为 (5, 128)# 然后通过线性层将其转换为词汇表大小的向量 (5, vocab_size)output = self.out(output.squeeze())# 返回输出和隐藏状态return output, hidden# 初始化隐藏状态的方法def init_hidden(self):# 返回一个全零的隐藏状态张量,维度为 (1, 1, 128)return torch.zeros(1, 1, 128).to(device)# 构建训练函数
def train(epoch, train_log):# 构建词典,返回索引到词,词到索引,词汇数量,和语料索引列表index_to_word, word_to_index, word_count, corpus_idx = build_vocab()# 创建歌词数据集实例,每个输入序列的长度为 32lyrics = LyricsDataset(corpus_idx, 32)# 初始化文本生成模型,词汇表大小为 word_countmodel = TextGenerator(word_count).to(device)# TODO 正式进入这一阶段代码# 定义交叉熵损失函数criterion = nn.CrossEntropyLoss()# 定义 Adam 优化器,学习率为 1e-3optimizer = optim.Adam(model.parameters(), lr=1e-3)# 打开日志文件用于写入file = open(train_log, 'w')# 开始训练循环for epoch_idx in range(epoch):# 数据加载器,打乱顺序,每次取 1 个样本lyrics_dataloader = DataLoader(lyrics, shuffle=True, batch_size=1)# 记录训练开始时间start = time.time()# 重置迭代次数iter_num = 0# 重置训练损失total_loss = 0.0# 遍历数据加载器中的数据for x, y in lyrics_dataloader:# 初始化隐藏状态hidden = model.init_hidden()# 前向传播计算输出和隐藏状态x, y = x.to(device), y.to(device)output, hidden = model(x, hidden)# 计算损失,y.squeeze() 去掉维度大小为1的维度# TODO 计算模型输出与实际目标之间的损失(误差)loss = criterion(output, y.squeeze())# 梯度清零optimizer.zero_grad()# 反向传播计算梯度loss.backward()# 更新参数optimizer.step()# 迭代次数加1iter_num += 1# 累加损失total_loss += loss.item()# 构建本次迭代的日志信息message = 'epoch %3s loss: %.5f time %.2f' % \(epoch_idx + 1,  # 当前训练轮数total_loss / iter_num,  # 平均损失time.time() - start)  # 本轮训练时间# 打印日志信息print(message)# 写入日志文件file.write(message + '\n')# 关闭日志文件file.close()# 保存模型参数到文件torch.save(model.state_dict(), 'model/lyrics_model_%d.bin' % epoch)# 构建预测函数
def predict(start_word, sentence_length, model_path):# 构建词典,返回索引到词,词到索引,词汇数量index_to_word, word_to_index, word_count, _ = build_vocab()# 构建文本生成模型实例,词汇表大小为 word_countmodel = TextGenerator(vocab_size=word_count).to(device)# 加载训练好的模型参数model.load_state_dict(torch.load(model_path))# 初始化隐藏状态hidden = model.init_hidden()try:# 将起始词转换为词索引word_idx = word_to_index[start_word]except:print("该词不在词典中, 请重新输入")return# 用于存储生成的句子(词索引序列)generate_sentence = [word_idx]# 生成长度为 sentence_length 的句子for _ in range(sentence_length):# 前向传播,获取模型输出和隐藏状态output, hidden = model(torch.tensor([[word_idx]]).to(device), hidden)# print("output: ", output)# 获取输出中概率最大的词的索引word_idx = torch.argmax(output).item()# 将该词索引添加到生成的句子中generate_sentence.append(word_idx)# 将生成的词索引序列转换为实际词并打印for idx in generate_sentence:print(index_to_word[idx], end='')print()if __name__ == '__main__':train(epoch=1, train_log='log/lyrics_training.log')  # 设置训练轮数为 200, 训练日志文件名为 lyrics_training.logpredict('分手', 15, 'model/lyrics_model_1.bin')

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 你真正了解低代码么?(国内低代码平台状况分析)
  • 华为od(D卷)最大N个数和最小N个数的和
  • 怎么用云手机进行TikTok矩阵运营
  • OpenTiny HUICharts 正式开源发布,一个简单、易上手的图表组件库
  • 【JAVA】获取object中 key对应的value值
  • 数据结构的基本概念
  • python Django中使用ORM进行分组统计并降序排列
  • 《计算机组成原理》(第3版)第3章 系统总线 复习笔记
  • 1009 Product of Polynomials(Java)
  • Spring Boot 3.x Rest API统一异常处理最佳实践
  • 解决多个Jenkins Master实例共享Jenkins_home目录的问题(加锁解锁机制)
  • 基于Hadoop的海量电商用户行为分析及机器学习购买预测研究【购物行为分析、100万条数据案例项目】
  • FPGA开发——IP核RAM的调用(双端口)
  • 轻闪PDF v2.14.9 解锁版下载及安装教程 (一款全能PDF转换器,支持自动补全标签)
  • 某通电子文档安全管理系统 CDGAuthoriseTempletService1接口SQL注入漏洞复现 [附POC]
  • CSS 提示工具(Tooltip)
  • Docker 笔记(1):介绍、镜像、容器及其基本操作
  • git 常用命令
  • leetcode讲解--894. All Possible Full Binary Trees
  • nfs客户端进程变D,延伸linux的lock
  • NSTimer学习笔记
  • Spark in action on Kubernetes - Playground搭建与架构浅析
  • Swoft 源码剖析 - 代码自动更新机制
  • 阿里云购买磁盘后挂载
  • 彻底搞懂浏览器Event-loop
  • 从零开始的无人驾驶 1
  • 多线程 start 和 run 方法到底有什么区别?
  • 模型微调
  • 如何用Ubuntu和Xen来设置Kubernetes?
  • 三分钟教你同步 Visual Studio Code 设置
  • 新书推荐|Windows黑客编程技术详解
  • 一天一个设计模式之JS实现——适配器模式
  • 【云吞铺子】性能抖动剖析(二)
  • MPAndroidChart 教程:Y轴 YAxis
  • 第二十章:异步和文件I/O.(二十三)
  • ​Java基础复习笔记 第16章:网络编程
  • #if和#ifdef区别
  • #在 README.md 中生成项目目录结构
  • (C语言)fread与fwrite详解
  • (delphi11最新学习资料) Object Pascal 学习笔记---第2章第五节(日期和时间)
  • (附源码)spring boot火车票售卖系统 毕业设计 211004
  • (汇总)os模块以及shutil模块对文件的操作
  • (经验分享)作为一名普通本科计算机专业学生,我大学四年到底走了多少弯路
  • (九十四)函数和二维数组
  • (三)centos7案例实战—vmware虚拟机硬盘挂载与卸载
  • (十六)Flask之蓝图
  • (四)js前端开发中设计模式之工厂方法模式
  • (转)Scala的“=”符号简介
  • (转贴)用VML开发工作流设计器 UCML.NET工作流管理系统
  • (总结)Linux下的暴力密码在线破解工具Hydra详解
  • .gitignore文件设置了忽略但不生效
  • .NET gRPC 和RESTful简单对比
  • .NET:自动将请求参数绑定到ASPX、ASHX和MVC(菜鸟必看)
  • .NET开源的一个小而快并且功能强大的 Windows 动态桌面软件 - DreamScene2
  • .NET开源项目介绍及资源推荐:数据持久层