第N6周:使用Word2vec实现文本分类

由上周文章可知原理,这里直接进行训练。

一、数据预处理

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文件的分隔符和是否有标题行。

  1. sep='\t':这个参数指定了CSV文件中不同字段之间的分隔符。在标准的CSV文件中,通常使用逗号,作为分隔符。然而,有些数据文件使用的是制表符\t(也称为tab分隔符)来分隔不同的字段。通过设置sep='\t',Pandas会使用制表符作为字段分隔符来读取文件。
  2. 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函数被用于将textslabels这两个列表中的元素配对。

texts = [text1, text2, text3, ...]  # 假设texts是一个包含文本数据的列表
labels = [label1, label2, label3, ...]  # 假设labels是一个包含标签数据的列表

zip(texts, labels)被调用时,它会创建一个迭代器,该迭代器在每次迭代时生成一个元组,每个元组包含来自texts的一个文本元素和来自labels的相应标签元素。例如,在第一次迭代时,它会生成(text1, label1),在第二次迭代时生成(text2, label2),依此类推。
对于textslabels中的每一对元素,将其作为元组(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)

这段代码的作用是构建一个基于词向量的词典,并将词典保存为文件。

  1. def average_vec(text):: 定义了一个函数average_vec,该函数接受一个字符串text作为输入,并返回一个100维的词向量。
  2. vec = np.zeros(100).reshape((1, 100)): 创建了一个形状为(1, 100)的零向量vec,其中100是词向量的维度。
  3. for word in text:: 遍历字符串text中的每个单词。
  4. try:: 尝试执行以下代码,如果出现异常则跳过。
  5. vec += w2v.wv[word].reshape((1, 100)): 如果word在词向量模型w2v中,则将其对应的词向量添加到vec中。w2v.wv[word]返回的是一个形状为(100,)的词向量,因此需要使用reshape((1, 100))将其转换为形状为(1, 100)的向量,以便与vec进行加法操作。
  6. except KeyError:: 如果word不在词向量模型w2v中,则捕获KeyError异常并继续循环。
  7. return vec: 返回平均后的词向量vec
  8. x_vec = np.concatenate([average_vec(z) for z in x]): 对于x中的每个元素z,调用average_vec函数,并将结果添加到列表中。然后使用np.concatenate将所有结果合并成一个单一的向量x_vec
  9. 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_pipelinelabel_pipeline是两个lambda函数,它们定义了如何处理文本和标签数据。

  1. text_pipeline = lambda x: average_vec(x):
    • text_pipeline是一个lambda函数,它接受一个参数x
    • average_vec(x)是一个函数调用,它将x作为参数传递给average_vec函数。
    • 因此,text_pipeline函数的作用是调用average_vec函数,并将它的输出作为返回值。
  2. label_pipeline = lambda x: label_name.index(x):
    • label_pipeline是一个lambda函数,它接受一个参数x
    • label_name.index(x)是一个函数调用,它使用label_name列表中的元素作为索引,查找xlabel_name中的索引位置。

因此,label_pipeline函数的作用是将标签字符串转换为其在label_name列表中的索引位置。在这段代码中,text_pipelinelabel_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 中的一个函数,用于限制梯度的范数(大小)在训练过程中。

  1. model.parameters(): 返回模型中所有可学习的参数(权重和偏置)的列表。这些参数将在训练过程中通过反向传播计算梯度。
  2. 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)

这段代码是用于模型训练过程中的学习率调整逻辑。

  1. if total_accu is not None and total_accu > val_acc::
    • 检查变量total_accu是否非空(即之前已经计算过验证集的准确率),并且total_accu是否大于当前验证集的准确率val_acc
    • 如果上述条件都满足,说明当前验证集的准确率比之前记录的最高准确率还要低,因此需要降低学习率。
  2. scheduler.step():
    • 调用学习率调度器schedulerstep()方法。这会根据调度器的规则来调整优化器的学习率。
    • 通常,学习率调度器会根据验证集上的性能来调整学习率,例如,当验证集性能下降时,学习率会减小,以防止模型过拟合。
  3. else::
    • 如果上述条件不满足,说明当前验证集的准确率至少与之前记录的最高准确率持平或更高。
    • 在这种情况下,不需要调整学习率,而是将当前验证集的准确率记录为最高准确率total_accu
  4. total_accu = val_acc:
    • 将当前验证集的准确率val_acc赋值给total_accu,这样它就记录了当前最高验证集的准确率。
  5. print('-' * 69):
    • 打印69个破折号,用来在控制台输出中分隔不同的epoch信息。
  6. print('| Epoch {:1d} | time:{:4.2f}s |':
    • 打印当前epoch的信息,包括epoch编号、训练时间等。
  7. 'valid_acc {:4.3f} valid_loss {:4.3f} | lr {:4.6f}'.format(epoch, time.time() - epoch_start_time, val_acc, val_loss, lr):
    • 使用格式化字符串打印验证集的准确率、损失和当前学习率。
  8. print('-' * 69):
    • 再次打印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

  • 8
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值