- 🍨 本文为🔗365天深度学习训练营 中的学习记录博客
- 🍖 原作者:K同学啊
由上周文章可知原理,这里直接进行训练。
一、数据预处理
1.加载数据
import torch
import torch.nn as nn
import torchvision
from torchvision import transforms, datasets
import os,PIL,pathlib,warnings
warnings.filterwarnings("ignore") #忽略警告信息
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
device
device(type=‘cuda’)
在Pandas的read_csv
函数中,sep='\t'
和header=None
是两个重要的参数,它们分别指定了CSV文件的分隔符和是否有标题行。
sep='\t'
:这个参数指定了CSV文件中不同字段之间的分隔符。在标准的CSV文件中,通常使用逗号,
作为分隔符。然而,有些数据文件使用的是制表符\t
(也称为tab分隔符)来分隔不同的字段。通过设置sep='\t'
,Pandas会使用制表符作为字段分隔符来读取文件。header=None
:这个参数告诉PandasCSV文件中没有标题行。通常CSV文件的第一行包含列的名称,这就是标题行。但是,如果文件中没有标题行,你需要设置header=None
,这样Pandas就不会错误地将第一行数据当作列名。在这种情况下,Pandas会自动生成整数索引作为列名(0, 1, 2, …)。
综上所述,train_data = pd.read_csv('./train.csv', sep='\t', header=None)
这行代码的意思是:读取当前目录下名为train.csv
的文件,该文件使用制表符\t
作为字段分隔符,并且文件中没有标题行。
import pandas as pd
train_data = pd.read_csv('./train.csv', sep='\t', header=None)
train_data.head()
在Python中,zip
函数用于将两个或多个序列(列表、元组、字符串等)中的元素配对成一个个元组。它返回一个zip对象,这是一个迭代器,可以遍历配对好的元素。zip
函数被用于将texts
和labels
这两个列表中的元素配对。
texts = [text1, text2, text3, ...] # 假设texts是一个包含文本数据的列表
labels = [label1, label2, label3, ...] # 假设labels是一个包含标签数据的列表
当zip(texts, labels)
被调用时,它会创建一个迭代器,该迭代器在每次迭代时生成一个元组,每个元组包含来自texts
的一个文本元素和来自labels
的相应标签元素。例如,在第一次迭代时,它会生成(text1, label1)
,在第二次迭代时生成(text2, label2)
,依此类推。
对于texts
和labels
中的每一对元素,将其作为元组(x, y)
返回。yield
关键字用于创建一个生成器,这意味着custom_data_iter
函数是一个生成器函数,它会逐个生成配对的数据,而不是一次性返回所有数据的列表。
最后,x = train_data[0].values[:]
和y = train_data[1].values[:]
这两行代码分别取出train_data
DataFrame中的第一列(文本数据)和第二列(标签数据),并将它们转换为NumPy数组。这些数组随后被传递给custom_data_iter
函数,以便生成配对的数据用于训练或处理。
def custom_data_iter(texts, labels):
for x, y in zip(texts, labels):
yield x, y
x = train_data[0].values[:]
y = train_data[1].values[:]
2.构建词典
安装gensim这个包可以参考这篇文章常用 镜像
先建立了一个空的模型, 再使用build_vocab方法构建x词典,根据词频高低加入到词典中
然后使用train训练模型
from gensim.models.word2vec import Word2Vec
import numpy as np
w2v = Word2Vec(vector_size=100, min_count=3)
w2v.build_vocab(x)
w2v.train(x, total_examples=w2v.corpus_count, epochs=20)
(2733285, 3663560)
这段代码的作用是构建一个基于词向量的词典,并将词典保存为文件。
def average_vec(text):
: 定义了一个函数average_vec
,该函数接受一个字符串text
作为输入,并返回一个100维的词向量。vec = np.zeros(100).reshape((1, 100))
: 创建了一个形状为(1, 100)
的零向量vec
,其中100是词向量的维度。for word in text:
: 遍历字符串text
中的每个单词。try:
: 尝试执行以下代码,如果出现异常则跳过。vec += w2v.wv[word].reshape((1, 100))
: 如果word
在词向量模型w2v
中,则将其对应的词向量添加到vec
中。w2v.wv[word]
返回的是一个形状为(100,)
的词向量,因此需要使用reshape((1, 100))
将其转换为形状为(1, 100)
的向量,以便与vec
进行加法操作。except KeyError:
: 如果word
不在词向量模型w2v
中,则捕获KeyError
异常并继续循环。return vec
: 返回平均后的词向量vec
。x_vec = np.concatenate([average_vec(z) for z in x])
: 对于x
中的每个元素z
,调用average_vec
函数,并将结果添加到列表中。然后使用np.concatenate
将所有结果合并成一个单一的向量x_vec
。w2v.save('./w2v_model.pkl')
: 将词向量模型w2v
保存到文件./w2v_model.pkl
中。这个模型可以用来加载词向量,以便在后续的文本处理任务中使用。
这段代码的主要目的是将文本中的单词转换为词向量,并计算这些词向量的平均值,以便在后续的文本处理任务中使用。同时,它还将词向量模型保存下来,以便在不同的应用程序中重复使用。
def average_vec(text):
vec = np.zeros(100).reshape((1, 100))
for word in text:
try:
vec += w2v.wv[word].reshape((1, 100))
except KeyError:
continue
return vec
x_vec = np.concatenate([average_vec(z) for z in x])
w2v.save('./w2v_model.pkl')
train_iter = custom_data_iter(x_vec, y)
len(x), len(x_vec)
(12100, 12100)
label_name = list(set(train_data[1].values))
print(label_name)
[‘Radio-Listen’, ‘Audio-Play’, ‘HomeAppliance-Control’, ‘TVProgram-Play’, ‘Music-Play’, ‘Weather-Query’, ‘Alarm-Update’, ‘Calendar-Query’, ‘FilmTele-Play’, ‘Other’, ‘Travel-Query’, ‘Video-Play’]
在Python中,lambda函数是一种匿名函数,它可以在一行中定义一个函数,而不需要使用def
关键字。lambda函数通常用于简单的、一次性的函数,特别是在需要一个简单的函数作为另一个函数的参数时。text_pipeline
和label_pipeline
是两个lambda函数,它们定义了如何处理文本和标签数据。
text_pipeline = lambda x: average_vec(x)
:text_pipeline
是一个lambda函数,它接受一个参数x
。average_vec(x)
是一个函数调用,它将x
作为参数传递给average_vec
函数。- 因此,
text_pipeline
函数的作用是调用average_vec
函数,并将它的输出作为返回值。
label_pipeline = lambda x: label_name.index(x)
:label_pipeline
是一个lambda函数,它接受一个参数x
。label_name.index(x)
是一个函数调用,它使用label_name
列表中的元素作为索引,查找x
在label_name
中的索引位置。
因此,label_pipeline
函数的作用是将标签字符串转换为其在label_name
列表中的索引位置。在这段代码中,text_pipeline
和label_pipeline
被用作数据预处理的步骤,它们将原始文本和标签转换为适合模型输入的格式。这些函数在数据加载过程中被应用,以确保模型接收到适当格式和类型的输入数据。
text_pipeline('我想打游戏')
array([[ 0.85660863, -0.5024803 , 0.49215668, 2.99310556, 1.80681494,
0.70676546, 3.39211745, -2.09852808, 0.56644397, -3.11135886,
6.5204747 , -1.98011923, -4.21444259, -0.31572193, -3.5315733 ,
-2.32329619, -1.4668256 , 2.57062118, 2.02882953, 0.18371885,
1.49574521, 1.84793383, 1.54830305, 2.35136663, -1.02046983,
-2.12544464, 0.99183828, 3.06154583, -1.56369609, -2.40042543,
3.18056703, 0.31088091, 3.01823376, -0.72959782, 3.82609385,
-2.81415109, -1.1632261 , 2.19537041, -0.53178678, -3.49869363,
3.24524245, -4.17508903, -0.78682132, -3.71646029, -1.18888205,
-4.41613352, -3.3447541 , 0.28300072, 1.80441998, 2.4975948 ,
-0.36943571, -1.16933751, -1.9540607 , -3.22651256, 1.32024156,
-2.17987895, 1.19763109, 0.86305595, -0.75016183, -1.43170713,
-1.037907 , 3.82205141, 0.58895477, -3.11131359, -2.81130632,
4.26393162, 5.38258564, 1.72494344, -4.95134321, 0.67513174,
0.53721979, -1.40989985, 5.31594515, -1.53908248, -1.88182026,
-3.7506457 , 1.53928459, -3.51880695, 3.26031524, -1.99472983,
0.26663119, -1.53545922, 0.38967106, 5.73795377, 3.80686732,
-1.51781213, -0.1521444 , -2.05778646, -1.49667389, 0.49015509,
-3.08246285, -1.81137607, 0.48227394, -1.81457248, -1.79541671,
0.61564708, -0.64064112, -1.21389153, -1.75515351, 2.12480001]])
label_pipeline("FilmTele-Play")
8
from torch.utils.data import DataLoader
def collate_batch(batch):
label_list, text_list= [], []
for (_text, _label) in batch:
label_list.append(label_pipeline(_label))
processed_text = torch.tensor(text_pipeline(_text), dtype=torch.float32)
text_list.append(processed_text)
label_list = torch.tensor(label_list, dtype=torch.int64)
text_list = torch.cat(text_list)
return text_list.to(device), label_list.to(device)
dataloader = DataLoader(train_iter,
batch_size=8,
shuffle = False,
collate_fn=collate_batch)
二、模型构建
1.模型搭建
这里没有去构建更复杂的模型
from torch import nn
class TextClassificationModel(nn.Module):
def __init__(self, num_class):
super(TextClassificationModel, self).__init__()
self.fc = nn.Linear(100, num_class)
def forward(self,text):
return self.fc(text)
2.初始化模型
num_class = len(label_name)
vocab_size = 100000
embedding_size = 12
model = TextClassificationModel(num_class).to(device)
3.定义训练和评估函数
这里其他的东西都耳熟能详了,其中梯度裁剪可以领出来讲一讲。
在深度学习中,梯度裁剪是一种常见的技术,用于防止梯度爆炸,这是神经网络训练过程中可能出现的问题之一。当梯度变得非常大时,它们可能会导致权重更新变得不稳定,甚至可能导致模型训练失败。
torch.nn.utils.clip_grad_norm_(model.parameters(), 0.1)
是 PyTorch 中的一个函数,用于限制梯度的范数(大小)在训练过程中。
model.parameters()
: 返回模型中所有可学习的参数(权重和偏置)的列表。这些参数将在训练过程中通过反向传播计算梯度。0.1
: 这是梯度裁剪的上限,也称为裁剪阈值。这个阈值通常是一个较小的数值,比如0.1、0.5或1.0。一旦梯度的范数超过这个阈值,PyTorch就会将其裁剪到这个阈值。
例如,如果一个参数的梯度是[10, 20, 30]
,其范数(即所有元素的平方和的平方根)是sqrt(10^2 + 20^2 + 30^2)
。如果这个范数超过了0.1,PyTorch就会将这个梯度裁剪到0.1
。
梯度裁剪的好处在于,它可以帮助模型稳定地训练,尤其是在使用高阶的激活函数(如ReLU)和层数较多的网络时。它还可以减少训练时间,因为裁剪后的梯度更新更加稳定,减少了模型在梯度爆炸时需要的时间来恢复稳定。
import time
def train(dataloader):
model.train() # 切换为训练模式
total_acc, train_loss, total_count = 0, 0, 0
log_interval = 50
start_time = time.time()
for idx, (text, label) in enumerate(dataloader):
predicted_label = model(text)
optimizer.zero_grad()
loss = criterion(predicted_label, label)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 0.1)
optimizer.step()
total_acc += (predicted_label.argmax(1) == label).sum().item()
train_loss += loss.item()
total_count += label.size(0)
if idx % log_interval == 0 and idx > 0:
elapsed = time.time() - start_time
print('| epoch{:1d} | {:4d}/{:4d} batches'
'| train acc {:4.3f}| train loss {:4.5f}'.format(epoch, idx, len(dataloader), total_acc/total_count, train_loss/total_count))
total_acc, train_loss, total_count = 0, 0, 0
start_time = time.time()
def evaluate(dataloader):
model.eval()
total_acc, train_loss, total_count = 0, 0, 0
with torch.no_grad():
for idx, (text, label) in enumerate(dataloader):
predicted_label = model(text)
loss = criterion(predicted_label, label)
total_acc += (predicted_label.argmax(1) == label).sum().item()
train_loss += loss.item()
total_count += label.size(0)
return total_acc/total_count, train_loss/total_count
这里有一个问题需要注意,大家在构建模型的时候很可能会忘了一个小细节,结果会导致这样的报错
这个报错来源于这行代码
total_count += label.size
这段代码的报错是由于在尝试将torch.Size
对象与整数相加时发生的类型不匹配。在PyTorch中,label.size()
返回的是一个torch.Size
对象,它表示张量(tensor)的尺寸。在这个上下文中,label.size()
返回的是标签张量的形状,例如(batch_size,)
,而total_count
是一个整数,用于累加总的样本数量。
错误发生在尝试将label.size()
的返回值(torch.Size
对象)与total_count
(整数)相加。由于torch.Size
对象不是整数,不能直接与整数相加。
为了解决这个问题,需要将label.size()
返回的torch.Size
对象转换为整数。这可以通过访问label.size()
返回对象的第一个元素(即label.size(0)
)来实现,因为对于标签张量来说,它的形状通常是一个一元组,其中的第一个元素就是批处理的大小(batch size)。
所以一个小小的0很容易被忽略,但是只要记住了它的功效,就不会遗漏这种小错误,同时根据这个你报错也就知道了错误在哪里,改正后的代码是这样的
total_count += label.size(0)
三、训练模型
1.拆分数据集并运行模型
from torch.utils.data.dataset import random_split
from torchtext.data.functional import to_map_style_dataset
# 超参数
EPOCHS = 10 # epoch
LR = 5 # 学习率
BATCH_SIZE = 64 # batch size for training
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=LR)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, 1.0, gamma=0.1)
total_accu = None
# 构建数据集
train_iter = custom_data_iter(train_data[0].values[:], train_data[1].values[:])
train_dataset = to_map_style_dataset(train_iter)
split_train_, split_valid_ = random_split(train_dataset, [int(len(train_dataset)*0.8), int(len(train_dataset)*0.2)])
train_dataloader = DataLoader(split_train_, batch_size=BATCH_SIZE, shuffle=True, collate_fn=collate_batch)
valid_dataloader = DataLoader(split_valid_, batch_size=BATCH_SIZE, shuffle=True, collate_fn=collate_batch)
for epoch in range(1, EPOCHS + 1):
epoch_start_time = time.time()
train(train_dataloader)
val_acc, val_loss = evaluate(valid_dataloader)
# 获取当前的学习率
lr = optimizer.state_dict()['param_groups'][0]['lr']
if total_accu is not None and total_accu > val_acc:
scheduler.step()
else:
total_accu = val_acc
print('-' * 69)
print('| Epoch {:1d} | time:{:4.2f}s |'
'valid_acc {:4.3f} valid_loss {:4.3f} | lr {:4.6f}'.format(epoch, time.time() - epoch_start_time, val_acc, val_loss, lr))
print('-' * 69)
我的输出是这样的
可能有人好奇epoch五六和八九十为什么没有虚线标注的部分。
主要是因为那个if else
if total_accu is not None and total_accu > val_acc:
scheduler.step()
else:
total_accu = val_acc
print('-' * 69)
print('| Epoch {:1d} | time:{:4.2f}s |'
'valid_acc {:4.3f} valid_loss {:4.3f} | lr {:4.6f}'.format(epoch, time.time() - epoch_start_time, val_acc, val_loss, lr))
print('-' * 69)
这段代码是用于模型训练过程中的学习率调整逻辑。
if total_accu is not None and total_accu > val_acc:
:- 检查变量
total_accu
是否非空(即之前已经计算过验证集的准确率),并且total_accu
是否大于当前验证集的准确率val_acc
。 - 如果上述条件都满足,说明当前验证集的准确率比之前记录的最高准确率还要低,因此需要降低学习率。
- 检查变量
scheduler.step()
:- 调用学习率调度器
scheduler
的step()
方法。这会根据调度器的规则来调整优化器的学习率。 - 通常,学习率调度器会根据验证集上的性能来调整学习率,例如,当验证集性能下降时,学习率会减小,以防止模型过拟合。
- 调用学习率调度器
else:
:- 如果上述条件不满足,说明当前验证集的准确率至少与之前记录的最高准确率持平或更高。
- 在这种情况下,不需要调整学习率,而是将当前验证集的准确率记录为最高准确率
total_accu
。
total_accu = val_acc
:- 将当前验证集的准确率
val_acc
赋值给total_accu
,这样它就记录了当前最高验证集的准确率。
- 将当前验证集的准确率
print('-' * 69)
:- 打印69个破折号,用来在控制台输出中分隔不同的epoch信息。
print('| Epoch {:1d} | time:{:4.2f}s |'
:- 打印当前epoch的信息,包括epoch编号、训练时间等。
'valid_acc {:4.3f} valid_loss {:4.3f} | lr {:4.6f}'.format(epoch, time.time() - epoch_start_time, val_acc, val_loss, lr)
:- 使用格式化字符串打印验证集的准确率、损失和当前学习率。
print('-' * 69)
:- 再次打印69个破折号,以分隔不同的epoch信息。
总的来说,这段代码的作用是在模型训练过程中监控验证集的性能,并根据验证集性能的变化来调整学习率。如果验证集性能下降,学习率会被调整以防止模型过拟合;如果验证集性能保持或提高,则学习率保持不变。
- 再次打印69个破折号,以分隔不同的epoch信息。
test_acc, test_loss = evaluate(valid_dataloader)
print('模型准确率为:{:5.4f}'.format(test_acc))
模型准确率为:0.8158
2.测试指定数据
def predict(text, text_pipeline):
with torch.no_grad():
text = torch.tensor(text_pipeline(text), dtype=torch.float32)
print(text.shape)
output = model(text)
return output.argmax(1).item()
ex_text_str = "随便播放一首专辑阁楼里的佛里的歌"
model = model.to("cpu")
print("该文本的类别是: %s" %label_name[predict(ex_text_str, text_pipeline)])
torch.Size([1, 100])
该文本的类别是: Music-Play