开篇

记录一下自己对卷积神经网络进行句子分类情况的实现,国外研究情况:

  1. Kim在Theano中的实现: https://github.com/yoonkim/CNN_sentence
  2. Denny Britz在Tensorflow中的实现: https://github.com/dennybritz/cnn-text-classification-tf
  3. Alexander Rakhlin在Keras中的实现: https://github.com/alexander-rakhlin/CNN-for-Sentence-Classification-in-Keras

由于他们都大致实现过卷积神经网络,我这里就不再去构建模型了,基于已有模型探索一下卷积神经网络怎么做文本情感分析的

要求

  • python 3
  • pytorch > 0.1
  • torchtext > 0.1
  • numpy

结果

尝试了两个数据集,MR和SST。

数据集 类别数 我训练的最佳结果 Kim的论文结果
MR 2 75.89%(CNN-rand-static) 76.1%(CNN-rand-nostatic)
SST 5 37.60%(CNN-rand-static) 45.0%(CNN-rand-nostatic)

还没有认真调整SST的超参数。

用法

bashCopy code
./main.py -h

或者

cssCopy code
python3 main.py -h

训练

bashCopy code
./main.py

会得到以下结果:

yamlCopy codeBatch[100] - loss: 0.655424  acc: 59.3750%
Evaluation - loss: 0.672396  acc: 57.6923%(615/1066) 

测试

加载数据时,分别选择MR本地线上SST数据集

train_iter, dev_iter = mr(text_field, label_field, device=-1, repeat=False)
train_iter**,** dev_iter**,** test_iter = sst(text_field**,** label_field**,** device=-1**,** repeat=False)

运行成功后,控制台会打印每一个batch对应的损失精度和准确率,如下图

image-20230417103715341

img

测试好后,每个训练好的mr和sst对应的模型快照目录分别为:

image-20230417104320091

预测

使用mr数据集预测:

注:预测文本必须由空格分隔,即使是标点符号。此外,文本应该比最大卷积核尺寸要长。快照选项表示模型从哪里加载。如果没有指定它,模型将从头开始训练。

python .\main.py -predict="No one understands me, they only pressure me to slander me, I want to die" -snapshot="D:\softwares\pythonProject\tencentflask\tencentflask\tencent_spider\cnn\snapshot\2023-03-13_11-24-41\best_steps_1600.pt"

预测结果:nagative,如下图所示

image-20230417105542168
python .\main.py -predict="Hello my dear ,Today is our wedding anniversary,I hope we can always be together, I love you so much ." -snapshot="D:\softwares\pythonProject\tencentflask\tencentflask\tencent_spider\cnn\snapshot\2023-03-13_11-24-41\best_steps_1600.pt"

预测结果:positive,如下图所示

image-20230417105701790

使用sst数据集测试:

python .\main.py -predict="I feel her absolute and 100%  love for me, I am very happy" -snapshot="D:\softwares\pythonProject\tencentflask\tencentflask\tencent_spider\cnn\snapshot\2023-03-13_10-44-01\best_steps_1200.pt"

预测结果:very positive,如下图所示

image-20230417110731173
python .\main.py -predict="No one understands me, I have never received love" -snapshot="D:\softwares\pythonProject\tencentflask\tencentflask\tencent_spider\cnn\snapshot\2023-03-13_10-44-01\best_steps_1200.pt"

预测结果:very nagative,如下图所示

image-20230417110341080

具体实现:

main.py

  1. 导入必要的 Python 库,包括 argparse、os、datetime、torch、torchtext.datasets、model、train 和 mydatasets 等。
  2. 使用 argparse 库解析命令行参数,包括学习率、训练周期数、批次大小、日志打印周期、测试周期、保存周期、快照保存路径、停止迭代的最大次数、最佳模型保存开关等。
  3. 定义了两个函数 sst 和 mr,用于加载数据集。其中 sst 函数可以加载 SST 数据集,mr 函数可以加载 MR 数据集。
  4. 加载数据,并使用 CNN_Text 类实现模型,该模型包含了卷积层和池化层等。模型的输入是一个文本数据集,输出是文本分类结果。
  5. 对模型进行训练,并对模型进行测试,最终输出训练和测试结果。
#! /usr/bin/env python
import os
import argparse
import datetime
import torch
# from torchtext import data
import torchtext.legacy.data as data
import torchtext.legacy.datasets as datasets


import model
import train
import mydatasets


parser = argparse.ArgumentParser(description='CNN text classificer')
# learning
parser.add_argument('-lr', type=float, default=0.001, help='initial learning rate [default: 0.001]')
parser.add_argument('-epochs', type=int, default=256, help='number of epochs for train [default: 256]')
parser.add_argument('-batch-size', type=int, default=64, help='batch size for training [default: 64]')
parser.add_argument('-log-interval',  type=int, default=1,   help='how many steps to wait before logging training status [default: 1]')
parser.add_argument('-test-interval', type=int, default=100, help='how many steps to wait before testing [default: 100]')
parser.add_argument('-save-interval', type=int, default=500, help='how many steps to wait before saving [default:500]')
parser.add_argument('-save-dir', type=str, default='snapshot', help='where to save the snapshot')
parser.add_argument('-early-stop', type=int, default=1000, help='iteration numbers to stop without performance increasing')
parser.add_argument('-save-best', type=bool, default=True, help='whether to save when get best performance')
# data 
parser.add_argument('-shuffle', action='store_true', default=False, help='shuffle the data every epoch')
# model
parser.add_argument('-dropout', type=float, default=0.5, help='the probability for dropout [default: 0.5]')
parser.add_argument('-max-norm', type=float, default=3.0, help='l2 constraint of parameters [default: 3.0]')
parser.add_argument('-embed-dim', type=int, default=128, help='number of embedding dimension [default: 128]')
parser.add_argument('-kernel-num', type=int, default=100, help='number of each kind of kernel')
parser.add_argument('-kernel-sizes', type=str, default='3,4,5', help='comma-separated kernel size to use for convolution')
parser.add_argument('-static', action='store_true', default=False, help='fix the embedding')
# device
parser.add_argument('-device', type=int, default=-1, help='device to use for iterate data, -1 mean cpu [default: -1]')
parser.add_argument('-no-cuda', action='store_true', default=False, help='disable the gpu')
# option
parser.add_argument('-snapshot', type=str, default=None, help='filename of model snapshot [default: None]')
parser.add_argument('-predict', type=str, default=None, help='predict the sentence given')
parser.add_argument('-test', action='store_true', default=False, help='train or test')
args = parser.parse_args()


# load SST dataset
def sst(text_field, label_field,  **kargs):
    train_data, dev_data, test_data = datasets.SST.splits(text_field, label_field, fine_grained=True)
    text_field.build_vocab(train_data, dev_data, test_data)
    label_field.build_vocab(train_data, dev_data, test_data)
    train_iter, dev_iter, test_iter = data.BucketIterator.splits(
                                        (train_data, dev_data, test_data), 
                                        batch_sizes=(args.batch_size, 
                                                     len(dev_data), 
                                                     len(test_data)),
                                        **kargs)
    return train_iter, dev_iter, test_iter 


# load MR dataset
def mr(text_field, label_field, **kargs):
    train_data, dev_data = mydatasets.MR.splits(text_field, label_field)
    text_field.build_vocab(train_data, dev_data)
    label_field.build_vocab(train_data, dev_data)
    train_iter, dev_iter = data.Iterator.splits(
                                (train_data, dev_data), 
                                batch_sizes=(args.batch_size, len(dev_data)),
                                **kargs)
    return train_iter, dev_iter


# load data
print("\nLoading data...")
text_field = data.Field(lower=True)
label_field = data.Field(sequential=False)
# train_iter, dev_iter = mr(text_field, label_field, device=-1, repeat=False)
train_iter, dev_iter, test_iter = sst(text_field, label_field, device=-1, repeat=False)
#

# update args and print
args.embed_num = len(text_field.vocab)
args.class_num = len(label_field.vocab) - 1
args.cuda = (not args.no_cuda) and torch.cuda.is_available(); del args.no_cuda
args.kernel_sizes = [int(k) for k in args.kernel_sizes.split(',')]
args.save_dir = os.path.join(args.save_dir, datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S'))

print("\nParameters:")
for attr, value in sorted(args.__dict__.items()):
    print("\t{}={}".format(attr.upper(), value))


# model
cnn = model.CNN_Text(args)
if args.snapshot is not None:
    print('\nLoading model from {}...'.format(args.snapshot))
    cnn.load_state_dict(torch.load(args.snapshot))

if args.cuda:
    torch.cuda.set_device(args.device)
    cnn = cnn.cuda()
        

# train or predict
if args.predict is not None:
    label = train.predict(args.predict, cnn, text_field, label_field, args.cuda)
    print('\n[Text]  {}\n[Label] {}\n'.format(args.predict, label))
elif args.test:
    try:
        train.eval(test_iter, cnn, args) 
    except Exception as e:
        print("\nSorry. The test dataset doesn't  exist.\n")
else:
    print()
    try:
        train.train(train_iter, dev_iter, cnn, args)
    except KeyboardInterrupt:
        print('\n' + '-' * 89)
        print('Exiting from training early')

model.py

  1. 首先导入 PyTorch 库,包括 nn、functional 和 Variable 等模块。
  2. 定义了一个名为 CNN_Text 的类,继承自 nn.Module 类,因此这是一个 PyTorch 模型。
  3. 在初始化方法 init 中,定义了模型的各个超参数,包括词汇表大小、嵌入维度、类别数量、卷积核数量、卷积核大小等。
  4. 定义了一个词嵌入层 self.embed,用于将输入的文本转换为向量表示。
  5. 定义了多个卷积层 self.convs,每个卷积层包含一个卷积核和一个 ReLU 激活函数。
  6. 定义了一个 dropout 层 self.dropout,用于在训练过程中随机丢弃一部分神经元,以防止过拟合。
  7. 定义了一个全连接层 self.fc1,将所有卷积层的输出连接起来并映射到类别数量。
  8. 在前向传播方法 forward 中,首先将输入的文本通过词嵌入层转换为向量表示。
  9. 然后将向量表示转换为 4D 张量,作为卷积层的输入,其中第二个维度表示通道数,因为这里只有一个通道,所以为 1。
  10. 对每个卷积层进行卷积运算,并使用 ReLU 激活函数进行非线性变换。
  11. 对每个卷积层的输出进行最大池化操作,得到一个向量表示。
  12. 将所有卷积层的输出连接起来,得到一个维度为 len(Ks)*Co 的向量。
  13. 使用 dropout 层随机丢弃一部分神经元,以防止过拟合。
  14. 最后将向量表示通过全连接层映射到类别数量,得到预测结果。
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.autograd import Variable


class CNN_Text(nn.Module):
    
    def __init__(self, args):
        super(CNN_Text, self).__init__()
        self.args = args 
        V = args.embed_num
        D = args.embed_dim
        C = args.class_num
        Ci = 1
        Co = args.kernel_num
        Ks = args.kernel_sizes
        self.embed = nn.Embedding(V, D)
        self.convs = nn.ModuleList([nn.Conv2d(Ci, Co, (K, D)) for K in Ks])
        self.dropout = nn.Dropout(args.dropout)
        self.fc1 = nn.Linear(len(Ks) * Co, C)
        if self.args.static:
            self.embed.weight.requires_grad = False

    def forward(self, x):
        x = self.embed(x)  # (N, W, D)
        x = x.unsqueeze(1)  # (N, Ci, W, D)
        x = [F.relu(conv(x)).squeeze(3) for conv in self.convs]  # [(N, Co, W), ...]*len(Ks)
        x = [F.max_pool1d(i, i.size(2)).squeeze(2) for i in x]  # [(N, Co), ...]*len(Ks)
        x = torch.cat(x, 1)
        x = self.dropout(x)  # (N, len(Ks)*Co)
        logit = self.fc1(x)  # (N, C)
        return logit

train.py

使用PyTorch深度学习框架,并定义了四个函数:train(训练函数)、eval(评估函数)、predict(预测函数)和save(保存函数)。

  • train(train_iter, dev_iter, model, args): 训练函数,接收训练数据迭代器、验证数据迭代器、模型和一些参数作为输入。使用Adam优化器对模型进行训练,计算交叉熵损失和精度,并在达到一定步数后保存模型和输出训练日志。
  • eval(data_iter, model, args): 评估函数,接收数据迭代器、模型和一些参数作为输入。用来评估模型的性能,计算损失和精度。
  • predict(text, model, text_field, label_feild, cuda_flag): 预测函数,接收一个文本字符串、模型、文本域、标签域和一个CUDA标志作为输入。将输入文本转换为模型的输入格式,使用模型进行推理,并返回预测标签。
  • save(model, save_dir, save_prefix, steps): 保存模型函数,接收模型、保存目录、保存前缀和步数作为输入。将模型参数保存在指定路径下。

其他的变量包括import语句导入了需要使用的库,torch.cudatorch.autograd分别用于控制GPU的使用和计算图构建,F.cross_entropy()是交叉熵损失函数,optimizer是Adam优化器,args是一些超参数和训练参数的集合,用于控制模型的训练过程。

import os
import sys
import torch
import torch.autograd as autograd
import torch.nn.functional as F


def train(train_iter, dev_iter, model, args):
    if args.cuda:
        model.cuda()

    optimizer = torch.optim.Adam(model.parameters(), lr=args.lr)

    steps = 0
    best_acc = 0
    last_step = 0
    for epoch in range(1, args.epochs+1):
        for batch in train_iter:
            model.train()
            feature, target = batch.text, batch.label
            feature.t_(), target.sub_(1)  # batch first, index align
            if args.cuda:
                feature, target = feature.cuda(), target.cuda()

            optimizer.zero_grad()
            logit = model(feature)
            loss = F.cross_entropy(logit, target)
            loss.backward()
            optimizer.step()

            steps += 1
            if steps % args.log_interval == 0:
                corrects = (torch.max(logit, 1)[1].view(target.size()).data == target.data).sum()
                accuracy = 100.0 * corrects/batch.batch_size
                sys.stdout.write(
                    '\rBatch[{}] - loss: {:.6f}  acc: {:.4f}%({}/{})'.format(steps, 
                                                                             loss.item(), 
                                                                             accuracy.item(),
                                                                             corrects.item(),
                                                                             batch.batch_size))
            if steps % args.test_interval == 0:
                dev_acc = eval(dev_iter, model, args)
                if dev_acc > best_acc:
                    best_acc = dev_acc
                    last_step = steps
                    if args.save_best:
                        save(model, args.save_dir, 'best', steps)
                else:
                    if steps - last_step >= args.early_stop:
                        print('early stop by {} steps.'.format(args.early_stop))
            elif steps % args.save_interval == 0:
                save(model, args.save_dir, 'snapshot', steps)


def eval(data_iter, model, args):
    model.eval()
    corrects, avg_loss = 0, 0
    for batch in data_iter:
        feature, target = batch.text, batch.label
        feature.t_(), target.sub_(1)  # batch first, index align
        if args.cuda:
            feature, target = feature.cuda(), target.cuda()
        logit = model(feature)
        loss = F.cross_entropy(logit, target, size_average=False)
        avg_loss += loss.item()
        corrects += (torch.max(logit, 1)
                     [1].view(target.size()).data == target.data).sum()

    size = len(data_iter.dataset)
    avg_loss /= size
    accuracy = 100.0 * corrects/size
    print('\nEvaluation - loss: {:.6f}  acc: {:.4f}%({}/{}) \n'.format(avg_loss, 
                                                                       accuracy, 
                                                                       corrects, 
                                                                       size))
    return accuracy
def predict(text, model, text_field, label_feild, cuda_flag):
    assert isinstance(text, str)
    model.eval()
    # text = text_field.tokenize(text)
    text = text_field.preprocess(text)
    text = [[text_field.vocab.stoi[x] for x in text]]
    x = torch.tensor(text)
    x = autograd.Variable(x)
    if cuda_flag:
        x = x.cuda()
    print(x)
    output = model(x)
    _, predicted = torch.max(output, 1)
    return label_feild.vocab.itos[predicted.item()+1]
def save(model, save_dir, save_prefix, steps):
    if not os.path.isdir(save_dir):
        os.makedirs(save_dir)
    save_prefix = os.path.join(save_dir, save_prefix)
    save_path = '{}_steps_{}.pt'.format(save_prefix, steps)
    torch.save(model.state_dict(), save_path)

mydatasets.py

定义了一个叫做 MR 的类,它是 TarDataset 类的子类,表示了在可下载的 tar 文件中读取文本分类数据集 MR 的过程。

TarDataset 类是用来处理 tar 文件的一个自定义的数据集类,通过继承它,MR 类可以直接使用 TarDataset 类中的 download_or_unzip 方法下载数据集,并使用其中的 urlfilenamedirname 属性指定下载链接、文件名和目录名。

MR 类的 __init__ 方法接收 text_fieldlabel_field 两个参数,用于指定文本数据和标签数据的字段类型,以及 pathexamples 参数,用于指定数据文件的路径和数据样例。在 __init__ 方法中,它使用 open 函数读取 tar 文件中的 rt-polarity.negrt-polarity.pos 两个文件,读取后使用 data.Example.fromlist 方法将每个数据样例处理为一个 Example 对象,并将其加入到 examples 列表中,最终通过调用父类 data.Dataset__init__ 方法完成实例化过程。

MR 类的 splits 方法用于根据指定的数据集划分比例,返回包含训练集和验证集的元组。在该方法中,先使用 download_or_unzip 方法下载数据集并使用 __init__ 方法读取所有数据样例,然后使用 random.shuffle 方法将样例顺序打乱,最后根据 dev_ratio 参数指定的比例将样例划分为训练集和验证集,返回一个元组。

import re
import os
import random
import tarfile
import urllib
from torchtext import legacy
# import torchtext.data as data
import torchtext.legacy.data as data
class TarDataset(legacy.data.Dataset):
    @classmethod
    def download_or_unzip(cls, root):
        path = os.path.join(root, cls.dirname)
        if not os.path.isdir(path):
            tpath = os.path.join(root, cls.filename)
            if not os.path.isfile(tpath):
                print('downloading')
                urllib.request.urlretrieve(cls.url, tpath)
            with tarfile.open(tpath, 'r') as tfile:
                print('extracting')
                tfile.extractall(root)
        return os.path.join(path, '')


class MR(TarDataset):

    url = 'https://www.cs.cornell.edu/people/pabo/movie-review-data/rt-polaritydata.tar.gz'
    filename = 'rt-polaritydata.tar.gz'
    dirname = 'rt-polaritydata'

    @staticmethod
    def sort_key(ex):
        return len(ex.text)

    def __init__(self, text_field, label_field, path=None, examples=None, **kwargs):
        def clean_str(string):
            string = re.sub(r"[^A-Za-z0-9(),!?\'\`]", " ", string)
            string = re.sub(r"\'s", " \'s", string)
            string = re.sub(r"\'ve", " \'ve", string)
            string = re.sub(r"n\'t", " n\'t", string)
            string = re.sub(r"\'re", " \'re", string)
            string = re.sub(r"\'d", " \'d", string)
            string = re.sub(r"\'ll", " \'ll", string)
            string = re.sub(r",", " , ", string)
            string = re.sub(r"!", " ! ", string)
            string = re.sub(r"\(", " \( ", string)
            string = re.sub(r"\)", " \) ", string)
            string = re.sub(r"\?", " \? ", string)
            string = re.sub(r"\s{2,}", " ", string)
            return string.strip()

        text_field.tokenize = lambda x: clean_str(x).split()
        fields = [('text', text_field), ('label', label_field)]

        if examples is None:
            path = self.dirname if path is None else path
            examples = []
            with open(os.path.join(path, 'rt-polarity.neg'), errors='ignore') as f:
                examples += [
                    data.Example.fromlist([line, 'negative'], fields) for line in f]
            with open(os.path.join(path, 'rt-polarity.pos'), errors='ignore') as f:
                examples += [
                    data.Example.fromlist([line, 'positive'], fields) for line in f]
        super(MR, self).__init__(examples, fields, **kwargs)

    @classmethod
    def splits(cls, text_field, label_field, dev_ratio=.1, shuffle=True, root='.', **kwargs):
        path = cls.download_or_unzip(root)
        examples = cls(text_field, label_field, path=path, **kwargs).examples
        if shuffle: random.shuffle(examples)
        dev_index = -1 * int(dev_ratio*len(examples))

        return (cls(text_field, label_field, examples=examples[:dev_index]),
                cls(text_field, label_field, examples=examples[dev_index:]))

遇到的问题:

一:版本问题

AttributeError: module 'torchtext.data' has no attribute 'Iterator'
AttributeError: module 'torchtext.data' has no attribute 'Dataset'
AttributeError: module 'torchtext.data' has no attribute 'Field'
AttributeError: module 'torchtext.data' has no attribute 'Example'

由于安装的版本torchtext为0.9.1,在torchtext0.9以上移除了Dataset,和Field,所以

# from torchtext import data修改为
from torchtext.legacy import data
# import torchtext.data as data修改为
import torchtext.legacy.data as data

二:下载超时问题

requests.exceptions.ProxyError: HTTPConnectionPool(host='127.0.0.1', port=10809): Max retries exceeded with url: http://nlp.stanford.edu/sentiment/trainDevTestTrees_PTB.zip (Caused by ProxyError('Cannot connect to proxy.', NewConnectionError('<urllib3.connection.HTTPConnection object at 0x000001E31A477BB0>: Failed to establish a new connection: [WinError 10061] 由于目标计算机积极拒绝,无法连接。')))

这个是因为短时间内请求多次导致的,解决办法等一会儿,或者使用代理ip去请求

三:缺少文件问题

使用sst模型快照去预测的时候,报如下错误

ImportError: DLL load failed while importing _sqlite3: 找不到指定的模块。

由于缺少 sqlite3.dll 文件或者系统路径中没有找到该文件导致的。,在https://www.sqlite.org/download.html 下载好

image-20230313150723560

解压后丢到anaconda的目录DDl里面

image-20230313150800448

参考文献