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

06-20 1859阅读

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

    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()
    

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

    在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)

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

    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_pipeline和label_pipeline是两个lambda函数,它们定义了如何处理文本和标签数据。

    1. 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 中的一个函数,用于限制梯度的范数(大小)在训练过程中。

    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
    

    这里有一个问题需要注意,大家在构建模型的时候很可能会忘了一个小细节,结果会导致这样的报错

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

    这个报错来源于这行代码

    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)
    

    我的输出是这样的

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

    可能有人好奇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。
      • 如果上述条件都满足,说明当前验证集的准确率比之前记录的最高准确率还要低,因此需要降低学习率。
      • 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信息。

                      总的来说,这段代码的作用是在模型训练过程中监控验证集的性能,并根据验证集性能的变化来调整学习率。如果验证集性能下降,学习率会被调整以防止模型过拟合;如果验证集性能保持或提高,则学习率保持不变。

    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

VPS购买请点击我

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

目录[+]