第N6周:使用Word2vec实现文本分类
- 🍨 本文为🔗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