精华内容
下载资源
问答
  • 文本情感分类

    2021-03-20 22:52:13
    文本情感分类 1. 案例介绍 为了对前面的word embedding这种常用的文本向量化的方法进行巩固,这里完成一个文本情感分类的案例 现在有一个经典的数据集IMDB数据集,地址:http://ai.stanford.edu/~amaas/data/...

    文本情感分类

    1. 案例介绍

    为了对前面的word embedding这种常用的文本向量化的方法进行巩固,这里完成一个文本情感分类的案例

    现在有一个经典的数据集IMDB数据集,地址:http://ai.stanford.edu/~amaas/data/sentiment/,这是一份包含了5万条流行电影的评论数据,其中训练集25000条,测试集25000条。数据格式如下:

    下图左边为名称,其中名称包含两部分,分别是序号和情感评分,(1-4为neg,5-10为pos),右边为评论内容

    根据上述的样本,需要使用pytorch完成模型,实现对评论情感进行预测

     

    2. 思路分析

    首先可以把上述问题定义为分类问题,情感评分分为1-10,10个类别(也可以理解为回归问题,这里当做分类问题考虑)。那么根据之前的经验,大致流程如下:

    1. 准备数据集

    2. 构建模型

    3. 模型训练

    4. 模型评估

    知道思路之后,那么我们一步步来完成上述步骤

    3. 准备数据集

    准备数据集和之前的方法一样,实例化dataset,准备dataloader,最终数据可以处理成如下格式:

    其中有两点需要注意:

    1. 如何完成基础的Dataset的构建和Dataloader的准备

    2. 每个batch中文本的长度不一致的问题如何解决

    3. 每个batch中的文本如何转化为数字序列

    3.1 基础Dataset的准备    【当Dataset中的返回input结果如果是字符串的时候,可以通过修改collate_fn解决异常问题】

    dataset.py

    import os
    import re
    from torch.utils.data import Dataset, DataLoader
    
    data_base_path = r'./aclImdb/'
    
    
    #  1.定义token的方法
    def tokenize(test):
        filters = ['!','"','#','$','%','&','\(','\)','\*','\+',',','-','\.','/',':',';','<','=','>','\?','@'
            ,'\[','\\','\]','^','_','`','\{','\|','\}','~','\t','\n','\x97','\x96','”','“',]
        text = re.sub("<.*?>", " ", test, flags=re.S)
        text = re.sub("|".join(filters), " ", test, flags=re.S)
        return [i.strip() for i in text.split()]
    
    
    #  2.准备dataset
    class ImdbDataset(Dataset):
        def __init__(self, mode):
            super().__init__()
            if mode == "train":
                text_path = [os.path.join(data_base_path, i) for i in ["train/neg", "train/pos"]]
            else:
                text_path = [os.path.join(data_base_path, i) for i in ["test/neg", "test/pos"]]
            self.total_file_path_list = []
            for i in text_path:
                self.total_file_path_list.extend([os.path.join(i, j) for j in os.listdir(i)])
    
        def __getitem__(self, item):
            cur_path = self.total_file_path_list[item]
            cur_filename = os.path.basename(cur_path)
            label = int(cur_filename.split("_")[-1].split(".")[0]) - 1  # 处理标题,获取标签label,转化为从[0-9]
            text = tokenize(open(cur_path).read().strip())  # 直接按照空格进行分词
            return label, text
    
        def __len__(self):
            return len(self.total_file_path_list)
    
    
    #  3.实例化,准别dataloader
    dataset = ImdbDataset(mode="train")
    dataloader = DataLoader(dataset=dataset, batch_size=2, shuffle=True)
    
    #  4.观察数输出结果
    for idx, (label, text) in enumerate(dataloader):
        print("idx:", idx)
        print("label:", label)
        print("text:", text)
        break
    

    此时运行是报错的,

    出现问题的原因在于Dataloader中的参数collate_fn

    collate_fn的默认值为torch自定义的default_collate,collate_fn的作用就是对每个batch进行处理,而默认的default_collate处理出错。

    解决问题的思路:

    手段1:考虑先把数据转化为数字序列,观察其结果是否符合要求,之前使用DataLoader并未出现类似错误

    手段2:考虑自定义一个collate_fn,观察结果

    这里使用方式2,自定义一个collate_fn,然后观察结果:

    def collate_fn(batch):
        #  batch是一个列表,其中是一个一个的元组,每个元组是dataset中_getitem__的结果
        batch = list(zip(*batch))
        labels = torch.tensor(batch[0], dtype=torch.int32)
        texts = batch[1]
        del batch
        return labels, texts

    修改后的总代码:

    dataset.py

    import os
    import re
    import torch
    from torch.utils.data import Dataset, DataLoader
    
    data_base_path = r'./aclImdb/'
    
    
    #  1.定义token的方法
    def tokenize(test):
        filters = ['!','"','#','$','%','&','\(','\)','\*','\+',',','-','\.','/',':',';','<','=','>','\?','@'
            ,'\[','\\','\]','^','_','`','\{','\|','\}','~','\t','\n','\x97','\x96','”','“',]
        text = re.sub("<.*?>", " ", test, flags=re.S)
        text = re.sub("|".join(filters), " ", test, flags=re.S)
        return [i.strip() for i in text.split()]
    
    
    #  2.准备dataset
    class ImdbDataset(Dataset):
        def __init__(self, mode):
            super().__init__()
            if mode == "train":
                text_path = [os.path.join(data_base_path, i) for i in ["train/neg", "train/pos"]]
            else:
                text_path = [os.path.join(data_base_path, i) for i in ["test/neg", "test/pos"]]
            self.total_file_path_list = []
            for i in text_path:
                self.total_file_path_list.extend([os.path.join(i, j) for j in os.listdir(i)])
    
        def __getitem__(self, item):
            cur_path = self.total_file_path_list[item]
            cur_filename = os.path.basename(cur_path)
            label = int(cur_filename.split("_")[-1].split(".")[0]) - 1  # 处理标题,获取标签label,转化为从[0-9]
            text = tokenize(open(cur_path).read().strip())  # 直接按照空格进行分词
            return label, text
    
        def __len__(self):
            return len(self.total_file_path_list)
    
    
    def collate_fn(batch):
        #  batch是一个列表,其中是一个一个的元组,每个元组是dataset中_getitem__的结果
        batch = list(zip(*batch))
        labels = torch.tensor(batch[0], dtype=torch.int32)
        texts = batch[1]
        del batch
        return labels, texts
    
    
    #  3.实例化,准别dataloader
    dataset = ImdbDataset(mode="train")
    dataloader = DataLoader(dataset=dataset, batch_size=2, shuffle=True, collate_fn=collate_fn)
    
    #  4.观察数输出结果
    for idx, (label, text) in enumerate(dataloader):
        print("idx:", idx)
        print("label:", label)
        print("text:", text)
        break
    

    运行结果:

    可以把上述代码的实例化部分封装成函数,示例代码如下,后面的代码是直接调用封装好的函数

    import os
    import re
    import torch
    from torch.utils.data import Dataset, DataLoader
    
    data_base_path = r'../data/aclImdb/'
    
    
    #  1.定义token的方法
    def tokenize(test):
        filters = ['!', '"', '#', '$', '%', '&', '\(', '\)', '\*', '\+', ',', '-', '\.', '/', ':', ';', '<', '=', '>', '\?',
                   '@'
            , '\[', '\\', '\]', '^', '_', '`', '\{', '\|', '\}', '~', '\t', '\n', '\x97', '\x96', '”', '“', ]
        text = re.sub("<.*?>", " ", test, flags=re.S)
        text = re.sub("|".join(filters), " ", test, flags=re.S)
        return [i.strip() for i in text.split()]
    
    
    #  2.准备dataset
    class ImdbDataset(Dataset):
        def __init__(self, mode):
            super().__init__()
            if mode == "train":
                text_path = [os.path.join(data_base_path, i) for i in ["train/neg", "train/pos"]]
            else:
                text_path = [os.path.join(data_base_path, i) for i in ["test/neg", "test/pos"]]
            self.total_file_path_list = []
            for i in text_path:
                self.total_file_path_list.extend([os.path.join(i, j) for j in os.listdir(i)])
    
        def __getitem__(self, item):
            cur_path = self.total_file_path_list[item]
            cur_filename = os.path.basename(cur_path)
            label = int(cur_filename.split("_")[-1].split(".")[0]) - 1  # 处理标题,获取标签label,转化为从[0-9]
            text = tokenize(open(cur_path).read().strip())  # 直接按照空格进行分词
            return label, text
    
        def __len__(self):
            return len(self.total_file_path_list)
    
    
    def collate_fn(batch):
        #  batch是一个列表,其中是一个一个的元组,每个元组是dataset中_getitem__的结果
        batch = list(zip(*batch))
        labels = torch.tensor(batch[0], dtype=torch.int32)
        texts = batch[1]
        del batch
        return labels, texts
    
    
    def get_dataloader():
        imdb_dataset = ImdbDataset('train')
        data_loader = DataLoader(imdb_dataset, shuffle=True, batch_size=2, collate_fn=collate_fn)
        return data_loader
    
    
    #  3.实例化,准别dataloader
    dataset = ImdbDataset(mode="train")
    dataloader = DataLoader(dataset=dataset, batch_size=2, shuffle=True, collate_fn=collate_fn)
    
    #  4.观察数输出结果
    # for idx, (label, text) in enumerate(dataloader):
    for idx, (label, text) in enumerate(get_dataloader()):
        print("idx:", idx)
        print("label:", label)
        print("text:", text)
        break
    

    3.2 文本序列化

    再介绍word embedding的时候,不会直接把文本转化为向量,而是先转化为数字,再把数字转化为向量,那么这个过程该如何实现呢?

    这里我们可以考虑把文本中的每个词语和其对应的数字,使用字典保存,同时实现方法把句子通过字典映射为包含数字的列表

    实现文本序列化之前,考虑以下几点:

    1. 如何使用字典把词语和数字进行对应

    2. 不同的词语出现的次数不尽相同,是否需要对高频或者低频词语进行过滤,以及总的词语数量是否需要进行限制

    3. 得到词典之后,如何把句子转化为数字序列,如何把数字序列转化为句子

    4. 不同句子长度不相同,每个batch的句子如何构造成相同的长度(可以对短句子进行填充,填充特殊字符)

    5. 对于新出现的词语在词典中没有出现怎么办(可以使用特殊字符代理)

    思路分析:

    1. 对所有句子进行分词

    2. 词语存入字典,根据次数对词语进行过滤,并统计次数

    3. 实现文本转数字序列的方法

    4. 实现数字序列转文本方法

    wordSequence.py

    import numpy as np
    
    
    #  构建字典,实现方法把句子转换成为数字序列和其翻转
    class Word2Sequence(object):
        UNK_TAG = "UNK"  # 表示特殊的字符
        PAD_TAG = "PAD"  # 表示对句子的填充
    
        UNK = 0
        PAD = 1
    
        def __init__(self):
            # 初始词典
            self.dict = {
                self.UNK_TAG: self.UNK,
                self.PAD_TAG: self.PAD  # 初始键值对
            }
            self.fited = False
    
        #  把单词转换为索引
        def to_index(self, word):
            """word -> index"""
            assert self.fited == True, "必须先进行fit操作"
            return self.dict.get(word, self.UNK)
    
        #  把索引转换为词语/字
        def to_word(self, index):
            """index -> word"""
            assert self.fited, "必须先进行fit操作"
            if index in self.inversed_dict:
                return self.inversed_dict[index]
            return self.UNK_TAG
    
        def __len__(self):
            return self(self.dict)
    
        def fit(self, sentences, min_count=1, max_count=None, max_feature=None):  # max_count最大不限制
            """
            把所有的句子放到词典中去,把单个句子放到dict中去
            :param sentence: [[word1,word2,word3],[word1,word3,wordn..],...]
            :param min_count:最小出现的次数
            :param max_count:最大出现的次数
            :param max_feature:总词语的最大数量  一个保留多少个单词
            :return:
            """
            count = {}
            for sentence in sentences:
                for a in sentence:
                    if a not in count:
                        count[a] = 0
                    count[a] += 1
            # 比最小的数量大和比最大的数量小的需要
            if min_count is not None:  # 删除count中词频小于min word
                count = {k: v for k, v in count.items() if v >= min_count}
            if max_count is not None:  # 删除词语大于max 的值
                count = {k: v for k, v in count.items() if v <= max_count}
    
            #  限制最大的数量
            if isinstance(max_feature, int):
                count = sorted(list(count.items()), key=lambda x: x[1])  # 排序, 根据什么进行排序
                if max_feature is not None and len(count) > max_feature:
                    count = count[-int(max_feature):]
                for w, _ in count:
                    self.dict[w] = len(self.dict)
            else:
                for w in sorted(count.keys()):
                    self.dict[w] = len(self.dict)
    
            self.fited = True
            #  准备一个index->word的字典
            self.inversed_dict = dict(zip(self.dict.values(), self.dict.keys()))
    
        #  把新来的句子转换成序列
        def transform(self, sentence, max_len=None):
            """
            实现把句子转化为数组(向量)
            :param sentence:
            :param max_len:
            :return:
            """
            assert self.fited, "必须先进行fit操作"
            if max_len is not None:
                r = [self.PAD] * max_len
            else:
                r = [self.PAD] * len(sentence)
            if max_len is not None and len(sentence) > max_len:
                sentence = sentence[:max_len]
            for index, word in enumerate(sentence):
                r[index] = self.to_index(word)
            return np.array(r, dtype=np.int64)
    
        def inverse_transform(self, indices):
            """
            实现从数组 转化为文字
            :param indices: [1,2,3....]
            :return:[word1,word2.....]
            """
            sentence = []
            for i in indices:
                word = self.to_word(i)
                sentence.append(word)
            return sentence
    
    
    if __name__ == '__main__':
        w2s = Word2Sequence()
        w2s.fit([
            ["你", "好", "么"],
            ["你", "好", "哦"]])
        print(w2s.dict)
        print(w2s.fited)
        print(w2s.transform(["你", "好", "嘛"]))
        print(w2s.transform(["你好嘛"], max_len=10))
    

    运行结果:

    完成了wordsequence之后,接下来就是保存现有样本中的数据字典,方便后续的使用。

    实现对IMDB数据的处理和保存

    #1. 对IMDB的数据记性fit操作
    def fit_save_word_sequence():
        from wordSequence import Word2Sequence
    
        ws = Word2Sequence()
        train_path = [os.path.join(data_base_path,i)  for i in ["train/neg","train/pos"]]
        total_file_path_list = []
        for i in train_path:
            total_file_path_list.extend([os.path.join(i, j) for j in os.listdir(i)])
        for cur_path in tqdm(total_file_path_list,ascii=True,desc="fitting"):
            ws.fit(tokenize(open(cur_path).read().strip()))
        ws.build_vocab()
        # 对wordSequesnce进行保存
        pickle.dump(ws,open("./model/ws.pkl","wb"))
    
    #2. 在dataset中使用wordsequence
    ws = pickle.load(open("./model/ws.pkl","rb"))
    
    def collate_fn(batch):
        MAX_LEN = 500 
        #MAX_LEN = max([len(i) for i in texts]) #取当前batch的最大值作为batch的最大长度
    
        batch = list(zip(*batch))
        labes = torch.tensor(batch[0],dtype=torch.int)
    
        texts = batch[1]
        #获取每个文本的长度
        lengths = [len(i) if len(i)<MAX_LEN else MAX_LEN for i in texts]
        texts = torch.tensor([ws.transform(i, MAX_LEN) for i in texts])
        del batch
        return labes,texts,lengths
    
    #3. 获取输出
    dataset = ImdbDataset(ws,mode="train")
        dataloader = DataLoader(dataset=dataset,batch_size=20,shuffle=True,collate_fn=collate_fn)
        for idx,(label,text,length) in enumerate(dataloader):
            print("idx:",idx)
            print("table:",label)
            print("text:",text)
            print("length:",length)
            break

    输出如下:

    idx: 0
    table: tensor([ 7,  4,  3,  8,  1, 10,  7, 10,  7,  2,  1,  8,  1,  2,  2,  4,  7, 10,
             1,  4], dtype=torch.int32)
    text: tensor([[ 50983,  77480,  82366,  ...,      1,      1,      1],
            [ 54702,  57262, 102035,  ...,  80474,  56457,  63180],
            [ 26991,  57693,  88450,  ...,      1,      1,      1],
            ...,
            [ 51138,  73263,  80428,  ...,      1,      1,      1],
            [  7022,  78114,  83498,  ...,      1,      1,      1],
            [  5353, 101803,  99148,  ...,      1,      1,      1]])
    length: [296, 500, 221, 132, 74, 407, 500, 130, 54, 217, 80, 322, 72, 156, 94, 270, 317, 117, 200, 379]

    思考:前面我们自定义了MAX_LEN作为句子的最大长度,如果我们需要把每个batch中的最长的句子长度作为当前batch的最大长度,该如何实现?

    4. 构建模型

    这里我们只练习使用word embedding,所以模型只有一层,即:

    1. 数据经过word embedding

    2. 数据通过全连接层返回结果,计算log_softmax

    import torch
    import torch.nn as nn
    import torch.nn.functional as F
    from torch import optim
    from build_dataset import get_dataloader,ws,MAX_LEN
    
    class IMDBModel(nn.Module):
        def __init__(self,max_len):
            super(IMDBModel,self).__init__()
            self.embedding = nn.Embedding(len(ws),300,padding_idx=ws.PAD) #[N,300]
            self.fc = nn.Linear(max_len*300,10)  #[max_len*300,10]
    
        def forward(self, x):
            embed = self.embedding(x) #[batch_size,max_len,300]
            embed = embed.view(x.size(0),-1)
            out = self.fc(embed)
            return F.log_softmax(out,dim=-1)

    5. 模型的训练和评估

    训练流程和之前相同

    1. 实例化模型,损失函数,优化器

    2. 遍历dataset_loader,梯度置为0,进行向前计算

    3. 计算损失,反向传播优化损失,更新参数

    train_batch_size = 128
    test_batch_size = 1000
    imdb_model = IMDBModel(MAX_LEN)
    optimizer = optim.Adam(imdb_model.parameters())
    criterion = nn.CrossEntropyLoss()
    
    def train(epoch):
        mode = True
        imdb_model.train(mode)
        train_dataloader =get_dataloader(mode,train_batch_size)
        for idx,(target,input,input_lenght) in enumerate(train_dataloader):
            optimizer.zero_grad()
            output = imdb_model(input)
            loss = F.nll_loss(output,target) #traget需要是[0,9],不能是[1-10]
            loss.backward()
            optimizer.step()
            if idx %10 == 0:
                print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                    epoch, idx * len(input), len(train_dataloader.dataset),
                           100. * idx / len(train_dataloader), loss.item()))
    
                torch.save(imdb_model.state_dict(), "model/mnist_net.pkl")
                torch.save(optimizer.state_dict(), 'model/mnist_optimizer.pkl')
                
     def test():
        test_loss = 0
        correct = 0
        mode = False
        imdb_model.eval()
        test_dataloader = get_dataloader(mode, test_batch_size)
        with torch.no_grad():
            for target, input, input_lenght in test_dataloader:
                output = imdb_model(input)
                test_loss  += F.nll_loss(output, target,reduction="sum")
                pred = torch.max(output,dim=-1,keepdim=False)[-1]
                correct = pred.eq(target.data).sum()
            test_loss = test_loss/len(test_dataloader.dataset)
            print('\nTest set: Avg. loss: {:.4f}, Accuracy: {}/{} ({:.2f}%)\n'.format(
                test_loss, correct, len(test_dataloader.dataset),
                100. * correct / len(test_dataloader.dataset)))
    
    if __name__ == '__main__':
        test()
        for i in range(3):
            train(i)
            test()
    

    这里我们仅仅使用了一层全连接层,其分类效果不会很好,这里重点是理解常见的模型流程和word embedding的使用方法

     

    展开全文
  • 循环神经网络实现文本情感分类之使用LSTM完成文本情感分类 1. 使用LSTM完成文本情感分类 在前面,使用了word embedding去实现了toy级别的文本情感分类,那么现在在这个模型中添加上LSTM层,观察分类效果。 为了...

    循环神经网络实现文本情感分类之使用LSTM完成文本情感分类

    1. 使用LSTM完成文本情感分类

    在前面,使用了word embedding去实现了toy级别的文本情感分类,那么现在在这个模型中添加上LSTM层,观察分类效果。

    为了达到更好的效果,对之前的模型做如下修改

    1. MAX_LEN = 200

    2. 构建dataset的过程,把数据转化为2分类的问题,pos为1,neg为0,否则25000个样本完成10个类别的划分数据量是不够的

    3. 在实例化LSTM的时候,使用dropout=0.5,在model.eval()的过程中,dropout自动会为0

    1.1 修改模型

    import torch
    import pickle
    import torch.nn as nn
    import torch.nn.functional as F
    
    ws = pickle.load(open('./model/ws.pkl', 'rb'))
    device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
    
    
    class IMDBLstmModel(nn.Module):
        def __init__(self):
            super().__init__()
            self.embedding_dim = 200
            self.hidden_size = 64
            self.num_layer = 2
            self.bidirectional = True
            self.bi_num = 2 if self.bidirectional else 1
            self.dropout = 0.5
            #  以上部分为超参数,可以自行修改
    
            self.embedding = nn.Embedding(len(ws), self.embedding_dim, padding_idx=ws.PAD)  # [N, 300]
            self.lstm = nn.LSTM(self.embedding_dim, self.hidden_size, self.num_layer, bidirectional=self.bidirectional,
                                dropout=self.dropout)
    
            #  使用两个全连接层,中间使用relu激活函数
            self.fc = nn.Linear(self.hidden_size * self.bi_num, 20)
            self.fc2 = nn.Linear(20, 2)
    
        def forward(self, x):
            x = self.embedding(x)
            x = x.permute(1, 0, 2)  # 进行轴交换
            h_0, c_0 = self.init_hidden_state(x.size(1))
            _, (h_n, c_0) = self.lstm(x, (h_0, c_0))
    
            #  只要最后一个lstm单元处理的结果,这里去掉了hidden_state
            out = torch.cat([h_n[-2, :, :], h_n[-1, :, :]], dim=-1)
            out = self.fc(out)
            out = F.relu(out)
            out = self.fc2(out)
            return F.log_softmax(out, dim=-1)
    
        def init_hidden_state(self, batch_size):
            h_0 = torch.rand(self.num_layer * self.bi_num, batch_size, self.hidden_size).to(device)
            c_0 = torch.rand(self.num_layer * self.bi_num, batch_size, self.hidden_size).to(device)
            return h_0, c_0
    

    2.2 完成训练和测试代码

    为了提高程序的运行速度,可以考虑把模型放在gup上运行,那么此时需要处理一下几点:

    1. device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

    2. model.to(device)

    3. 除了上述修改外,涉及计算的所有tensor都需要转化为CUDA的tensor

      1. 初始化的h_0,c_0

      2. 训练集和测试集的input,traget

    4. 在最后可以通过tensor.cpu()转化为torch的普通tensor

    from torch import optim
    
    train_batch_size = 64
    test_batch_size = 5000
    # imdb_model = IMDBLstmModel(MAX_LEN)  # 基础model
    imdb_model = IMDBLstmModel().to(device)  # 在GPU上运行,提高运行速度
    # imdb_model.load_state_dict(torch.load("model/
    optimizer = optim.Adam(imdb_model.parameters())
    criterion = nn.CrossEntropyLoss()
    
    
    def train(epoch):
        mode = True
        imdb_model.train(mode)
        train_dataloader = get_dataloader(mode, train_batch_size)
        for idx, (target, input, input_length) in enumerate(train_dataloader):
            target = target.to(device)
            input = input.to(device)
            optimizer.zero_grad()
            output = imdb_model(input)
            loss = F.nll_loss(output, target)  # target需要是[0,9],不能是[1-10]
            loss.backward()
            optimizer.step()
            if idx % 10 == 0:
                pred = torch.max(output, dim=-1, keepdim=False)[-1]
                acc = pred.eq(target.data).cpu().numpy().mean() * 100.  # 使用eq判断是否一致
                print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}\t ACC: {:.6f}'.format(epoch, idx * len(input),
                                                                                             len(train_dataloader.dataset),
                                                                                             100. * idx / len(
                                                                                                 train_dataloader),
                                                                                             loss.item(), acc))
    
                torch.save(imdb_model.state_dict(), "model/mnist_net.pkl")
                torch.save(optimizer.state_dict(), 'model/mnist_optimizer.pkl')
    
    
    def test():
        mode = False
        imdb_model.eval()
        test_dataloader = get_dataloader(mode, test_batch_size)
        with torch.no_grad():
            for idx, (target, input, input_lenght) in enumerate(test_dataloader):
                target = target.to(device)
                input = input.to(device)
                output = imdb_model(input)
                test_loss = F.nll_loss(output, target, reduction="mean")
                pred = torch.max(output, dim=-1, keepdim=False)[-1]
                correct = pred.eq(target.data).sum()
                acc = 100. * pred.eq(target.data).cpu().numpy().mean()
                print('idx: {} Test set: Avg. loss: {:.4f}, Accuracy: {}/{} ({:.2f}%)\n'.format(idx, test_loss, correct,
                                                                                                target.size(0), acc))
    
    
    if __name__ == "__main__":
        test()
        for i in range(10):
            train(i)
            test()
    

    2.3 模型训练的最终输出

    ...
    Train Epoch: 9 [20480/25000 (82%)]	Loss: 0.017165	 ACC: 100.000000
    Train Epoch: 9 [21120/25000 (84%)]	Loss: 0.021572	 ACC: 98.437500
    Train Epoch: 9 [21760/25000 (87%)]	Loss: 0.058546	 ACC: 98.437500
    Train Epoch: 9 [22400/25000 (90%)]	Loss: 0.045248	 ACC: 98.437500
    Train Epoch: 9 [23040/25000 (92%)]	Loss: 0.027622	 ACC: 98.437500
    Train Epoch: 9 [23680/25000 (95%)]	Loss: 0.097722	 ACC: 95.312500
    Train Epoch: 9 [24320/25000 (97%)]	Loss: 0.026713	 ACC: 98.437500
    Train Epoch: 9 [15600/25000 (100%)]	Loss: 0.006082	 ACC: 100.000000
    idx: 0 Test set: Avg. loss: 0.8794, Accuracy: 4053/5000 (81.06%)
    idx: 1 Test set: Avg. loss: 0.8791, Accuracy: 4018/5000 (80.36%)
    idx: 2 Test set: Avg. loss: 0.8250, Accuracy: 4087/5000 (81.74%)
    idx: 3 Test set: Avg. loss: 0.8380, Accuracy: 4074/5000 (81.48%)
    idx: 4 Test set: Avg. loss: 0.8696, Accuracy: 4027/5000 (80.54%)

    可以看到模型的测试准确率稳定在81%左右。

    大家可以把上述代码改为GRU,或者多层LSTM继续尝试,观察效果

    完整代码:

    目录结构:

    main.py

    # 由于pickle特殊性,需要在此导入Word2Sequence
    from word_squence import Word2Sequence
    import pickle
    import os
    from dataset import tokenlize
    from tqdm import tqdm  # 显示当前迭代进度
    
    TRAIN_PATH = r"../data/aclImdb/train"
    
    if __name__ == '__main__':
        ws = Word2Sequence()
        temp_data_path = [os.path.join(TRAIN_PATH, 'pos'), os.path.join(TRAIN_PATH, 'neg')]
        for data_path in temp_data_path:
            # 获取每一个文件的路径
            file_paths = [os.path.join(data_path, file_name) for file_name in os.listdir(data_path)]
            for file_path in tqdm(file_paths):
                sentence = tokenlize(open(file_path, errors='ignore').read())
                ws.fit(sentence)
        ws.build_vocab(max=10, max_features=10000)
        pickle.dump(ws, open('./model/ws.pkl', 'wb'))
        print(len(ws.dict))
    

    model.py

    """
    定义模型
    模型优化方法:
    # 为使得结果更好 添加一个新的全连接层,作为输出,激活函数处理
    # 把双向LSTM的output传给一个单向LSTM再进行处理
    
    lib.max_len = 200
    lib.embedding_dim = 100  # 用长度为100的向量表示一个词
    lib.hidden_size = 128  # 每个隐藏层中LSTM单元个数
    lib.num_layer = 2  # 隐藏层数量
    lib.bidirectional = True  # 是否双向LSTM
    lib.dropout = 0.3  # 在训练时以一定的概率使神经元失活,实际上就是让对应神经元的输出为0
    lib.device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
    """
    import torch.nn as nn
    from lib import ws
    import torch.nn.functional as F
    from torch.optim import Adam
    from dataset import get_dataloader
    from tqdm import tqdm
    import torch
    import numpy as np
    import lib
    import os
    
    
    class Mymodel(nn.Module):
        def __init__(self):
            super().__init__()
            # nn.Embedding(num_embeddings - 词嵌入字典大小即一个字典里要有多少个词,embedding_dim - 每个词嵌入向量的大小。)
            self.embedding = nn.Embedding(len(ws), 100)
            # 加入LSTM
            self.lstm = nn.LSTM(input_size=lib.embedding_dim, hidden_size=lib.hidden_size, num_layers=lib.num_layer,
                                batch_first=True, bidirectional=lib.bidirectional, dropout=lib.dropout)
            self.fc = nn.Linear(lib.hidden_size * 2, 2)
    
        def forward(self, input):
            """
            :param input: 形状[batch_size, max_len]
            :return:
            """
            x = self.embedding(input)  # 进行embedding,形状[batch_size, max_len, 100]
    
            # x [batch_size, max_len, num_direction*hidden_size]
            # h_n[num_direction * num_layer, batch_size, hidden_size]
            x, (h_n, c_n) = self.lstm(x)
            # 获取两个方向最后一次的output(正向最后一个和反向第一个)进行concat
            # output = x[:,-1,:hidden_size]   前向,等同下方
            output_fw = h_n[-2, :, :]  # 正向最后一次输出
            # output = x[:,0,hidden_size:]   反向,等同下方
            output_bw = h_n[-1, :, :]  # 反向最后一次输出
            #  只要最后一个lstm单元处理的结果,这里去掉了hidden state
            output = torch.cat([output_fw, output_bw], dim=-1)  # [batch_size, hidden_size*num_direction]
    
            out = self.fc(output)
    
            return F.log_softmax(out, dim=-1)
    
    
    model = Mymodel()
    optimizer = Adam(model.parameters(), lr=0.01)
    if os.path.exists('./model/model.pkl'):
        model.load_state_dict(torch.load('./model/model.pkl'))
        optimizer.load_state_dict(torch.load('./model/optimizer.pkl'))
    
    
    # 训练
    def train(epoch):
        for idx, (input, target) in enumerate(get_dataloader(train=True)):
            output = model(input)
            optimizer.zero_grad()
            loss = F.nll_loss(output, target)
            loss.backward()
            optimizer.step()
            print(loss.item())
            print('当前第%d轮,idx为%d 损失为:%lf, ' % (epoch, idx, loss.item()))
    
            # 保存模型
            if idx % 100 == 0:
                torch.save(model.state_dict(), './model/model.pkl')
                torch.save(optimizer.state_dict(), './model/optimizer.pkl')
    
    
    # 评估
    def test():
        acc_list = []
        loss_list = []
        # 开启模型评估模式
        model.eval()
        # 获取测试集数据
        test_dataloader = get_dataloader(train=False)
        # tqdm(total = 总数,ascii = #,desc=描述)
        for idx, (input, target) in tqdm(enumerate(test_dataloader), total=len(test_dataloader), ascii=True, desc='评估:'):
            with torch.no_grad():
                output = model(input)
                # 计算当前损失
                cur_loss = F.nll_loss(output, target)
                loss_list.append(cur_loss)
                pred = output.max(dim=-1)[-1]
                # 计算当前准确率
                cur_acc = pred.eq(target).float().mean()
                acc_list.append(cur_acc)
        print('准确率为:%lf, 损失为:%lf' % (np.mean(acc_list), np.mean(loss_list)))
    
    
    if __name__ == '__main__':
        for i in tqdm(range(10)):
            train(i)
        test()
    

    dataset.py:

    import torch
    from torch.utils.data import Dataset, DataLoader
    import os
    import re
    
    """
    完成数据集准备
    """
    TRAIN_PATH = r"..\data\aclImdb\train"
    TEST_PATH = r"..\data\aclImdb\test"
    
    
    # 分词
    def tokenlize(content):
        content = re.sub(r"<.*?>", " ", content)
        filters = ['!', '"', '#', '$', '%', '&', '\(', '\)', '\*', '\+', ',', '-', '\.', '/', ':', ';', '<', '=', '>', '\?',
                   '@', '\[', '\\', '\]', '^', '_', '`', '\{', '\|', '\}', '~', '\t', '\n', '\x97', '\x96', '”', '“', ]
        content = re.sub("|".join(filters), " ", content)
        tokens = [i.strip().lower() for i in content.split()]
        return tokens
    
    
    class ImbdDateset(Dataset):
        def __init__(self, train=True):
            self.train_data_path = TRAIN_PATH
            self.test_data_path = TEST_PATH
            # 通过train和data_path控制读取train或者test数据集
            data_path = self.train_data_path if train else self.test_data_path
            # 把所有文件名放入列表
            # temp_data_path = [data_path + '/pos', data_path + '/neg']
            temp_data_path = [os.path.join(data_path, 'pos'), os.path.join(data_path, 'neg')]
            self.total_file_path = []  # 所有pos,neg评论文件的path
            # 获取每个文件名字,并拼接路径
            for path in temp_data_path:
                file_name_list = os.listdir(path)
                file_path_list = [os.path.join(path, i) for i in file_name_list if i.endswith('.txt')]
                self.total_file_path.extend(file_path_list)
    
        def __getitem__(self, index):
            # 获取index的path
            file_path = self.total_file_path[index]
            # 获取label
            label_str = file_path.split('\\')[-2]
            label = 0 if label_str == 'neg' else 1
            # 获取content
            tokens = tokenlize(open(file_path, errors='ignore').read())
            return tokens, label
    
        def __len__(self):
            return len(self.total_file_path)
    
    
    def get_dataloader(train=True):
        imdb_dataset = ImbdDateset(train)
        data_loader = DataLoader(imdb_dataset, shuffle=True, batch_size=128, collate_fn=collate_fn)
        return data_loader
    
    
    # 重新定义collate_fn
    def collate_fn(batch):
        """
        :param batch: (一个__getitem__[tokens, label], 一个__getitem__[tokens, label],..., batch_size个)
        :return:
        """
        content, label = list(zip(*batch))
        from lib import ws, max_len
        content = [ws.transform(i, max_len=max_len) for i in content]
        content = torch.LongTensor(content)
        label = torch.LongTensor(label)
        return content, label
    
    
    if __name__ == '__main__':
        for idx, (input, target) in enumerate(get_dataloader()):
            print(idx)
            print(input)
            print(target)
            break
    

    word_squence.py

    import numpy as np
    
    """
    构建词典,实现方法把句子转换为序列,和其翻转
    """
    
    
    class Word2Sequence(object):
        # 2个特殊类属性,标记特殊字符和填充标记
        UNK_TAG = 'UNK'
        PAD_TAG = 'PAD'
    
        UNK = 0
        PAD = 1
    
        def __init__(self):
            self.dict = {
                # 保存词语和对应的数字
                self.UNK_TAG: self.UNK,
                self.PAD_TAG: self.PAD
            }
            self.count = {}  # 统计词频
    
        def fit(self, sentence):
            """
            把单个句子保存到dict中
            :param sentence: [word1, word2 , ... , ]
            :return:
            """
            for word in sentence:
                # 对word出现的频率进行统计,当word不在sentence时,返回值是0,当word在sentence中时,返回+1,以此进行累计计数
                self.count[word] = self.count.get(word, 0) + 1
    
        def build_vocab(self, min=5, max=None, max_features=None):
            """
            生成词典
            :param min:最小词频数
            :param max:最大词频数
            :param max_feature:一共保留多少词语
            :return:
            """
            # 删除count < min 的词语,即保留count > min 的词语
            if min is not None:
                self.count = {word: value for word, value in self.count.items() if value > min}
            # 删除count > min 的词语,即保留count < max 的词语
            if max is not None:
                self.count = {word: value for word, value in self.count.items() if value < max}
            # 限制保留的词语数
            if max_features is not None:
                # sorted 返回一个列表[(key1, value1), (key2, value2),...],True为升序
                temp = sorted(self.count.items(), key=lambda x: x[-1], reverse=True)[:max_features]
                self.count = dict(temp)
                for word in self.count:
                    self.dict[word] = len(self.dict)
    
            # 得到一个翻转的dict字典
            # zip方法要比{value: word for word, value in self.dict.items()}快
            self.inverse_dict = dict(zip(self.dict.values(), self.dict.keys()))
    
        def transform(self, sentence, max_len=None):
            """
            把句子转换为序列
            :param sentence: [word1, word2...]
            :param max_len: 对句子进行填充或者裁剪
            :return:
            """
            if max_len is not None:
                # 句子长度小于最大长度,进行填充
                if max_len > len(sentence):
                    sentence = sentence + [self.PAD_TAG] * (max_len - len(sentence))
                # 句子长度大于最大长度,进行裁剪
                if max_len < len(sentence):
                    sentence = sentence[:max_len]
            # for word in sentence:
            #     self.dict.get(word, self.UNK)
            # 字典的get(key, default=None) 如果指定键不存在,则返回默认值None。
            return [self.dict.get(word, self.UNK) for word in sentence]
    
        def inverse_transform(self, indices):
            """
            把序列转换为句子
            :param indices: [1, 2, 3, ...]
            :return:
            """
            return [self.inverse_dict.get(idx) for idx in indices]
    
        def __len__(self):
            return len(self.dict)
    
    
    if __name__ == '__main__':
        ws = Word2Sequence()
        ws.fit(["我", "是", "我"])
        ws.fit(["我", "是", "谁"])
        ws.build_vocab(min=1, max_features=5)
        print(ws.dict)
        ret = ws.transform(['我', '爱', '北京'], max_len=10)
        print(ret)
        print(ws.inverse_transform(ret))
    

    lib.py

    import pickle
    import torch
    
    ws = pickle.load(open('./model/ws.pkl', 'rb'))
    
    max_len = 200
    embedding_dim = 100  # 用长度为100的向量表示一个词
    hidden_size = 128  # 每个隐藏层中LSTM单元个数
    num_layer = 2  # 隐藏层数量
    bidirectional = True  # 是否双向LSTM
    dropout = 0.3  # 在训练时以一定的概率使神经元失活,实际上就是让对应神经元的输出为0
    
    device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
    

     

    展开全文
  • 在做“亮剑杯”的时候,由于我还是初涉,水平有限,仅仅是基于传统的思路实现了一个简单的文本情感分类模型。而在后续的“泰迪杯”中,由于学习的深入,我已经基本了解深度学习的思想,并且用深度学习的算法实现了...

    前言:四五月份的时候,我参加了两个数据挖掘相关的竞赛,分别是物电学院举办的“亮剑杯”,以及第三届 “泰迪杯”全国大学生数据挖掘竞赛。很碰巧的是,两个比赛中,都有一题主要涉及到中文情感分类工作。在做“亮剑杯”的时候,由于我还是初涉,水平有限,仅仅是基于传统的思路实现了一个简单的文本情感分类模型。而在后续的“泰迪杯”中,由于学习的深入,我已经基本了解深度学习的思想,并且用深度学习的算法实现了文本情感分类模型。因此,我打算将两个不同的模型都放到博客中,供读者参考。刚入门的读者,可以从中比较两者的不同,并且了解相关思路。高手请一笑置之。

    基于情感词典的文本情感分类

    传统的基于情感词典的文本情感分类,是对人的记忆和判断思维的最简单的模拟,如上图。我们首先通过学习来记忆一些基本词汇,如否定词语有“不”,积极词语有“喜欢”、“爱”,消极词语有“讨厌”、“恨”等,从而在大脑中形成一个基本的语料库。然后,我们再对输入的句子进行最直接的拆分,看看我们所记忆的词汇表中是否存在相应的词语,然后根据这个词语的类别来判断情感,比如“我喜欢数学”,“喜欢”这个词在我们所记忆的积极词汇表中,所以我们判断它具有积极的情感。

    基于上述思路,我们可以通过以下几个步骤实现基于情感词典的文本情感分类:预处理、分词、训练情感词典、判断,整个过程可以如下图所示。而检验模型用到的原材料,包括薛云老师提供的蒙牛牛奶的评论,以及从网络购买的某款手机的评论数据(见附件)。

    文本的预处理

    由网络爬虫等工具爬取到的原始语料,通常都会带有我们不需要的信息,比如额外的Html标签,所以需要对语料进行预处理。由薛云老师提供的蒙牛牛奶评论也不例外。我们队伍使用Python作为我们的预处理工具,其中的用到的库有Numpy和Pandas,而主要的文本工具为正则表达式。经过预处理,原始语料规范为如下表,其中我们用-1标注消极情感评论,1标记积极情感评论。

    01⋮11711172⋮comment蒙牛又出来丢人了珍爱生命远离蒙牛⋮我一直都很爱喝蒙牛的纯牛奶一直,很爱送蒙牛...健康才是最好的礼物。⋮mark−1−1⋮11⋮commentmark0蒙牛又出来丢人了−11珍爱生命远离蒙牛−1⋮⋮⋮1171我一直都很爱喝蒙牛的纯牛奶一直,很爱11172送蒙牛...健康才是最好的礼物。1⋮⋮⋮

    句子自动分词

    为了判断句子中是否存在情感词典中相应的词语,我们需要把句子准确切割为一个个词语,即句子的自动分词。我们对比了现有的分词工具,综合考虑了分词的准确性和在Python平台的易用性,最终选择了“结巴中文分词”作为我们的分词工具。

    下表仅展示各常见的分词工具对其中一个典型的测试句子的分词效果:

    测试句子:工信处女干事每月经过下属科室都要亲口交代24口交换机等技术性器件的安装工作

    分词工具

    测试结果

    结巴中文分词

    工信处/ 女干事/ 每月/ 经过/ 下属/ 科室/ 都/ 要/ 亲口/ 交代/ 24/ 口/ 交换机/ 等/ 技术性/ 器件/ 的/ 安装/ 工作

    中科院分词

    工/n 信/n 处女/n 干事/n 每月/r 经过/p 下属/v 科室/n 都/d 要/v 亲口/d 交代/v 24/m 口/q 交换机/n 等/udeng 技术性/n 器件/n 的/ude1 安装/vn 工作/vn

    smallseg

    工信/ 信处/ 女干事/ 每月/ 经过/ 下属/ 科室/ 都要/ 亲口/ 交代/ 24/ 口/ 交换机/ 等/ 技术性/ 器件/ 的/ 安装/ 工作

    Yaha 分词

    工信处 / 女 / 干事 / 每月 / 经过 / 下属 / 科室 / 都 / 要 / 亲口 / 交代 / 24 / 口 / 交换机 / 等 / 技术性 / 器件 / 的 / 安装 / 工作

    载入情感词典

    一般来说,词典是文本挖掘最核心的部分,对于文本感情分类也不例外。情感词典分为四个部分:积极情感词典、消极情感词典、否定词典以及程度副词词典。为了得到更加完整的情感词典,我们从网络上收集了若干个情感词典,并且对它们进行了整合去重,同时对部分词语进行了调整,以达到尽可能高的准确率。

    我们队伍并非单纯对网络收集而来的词典进行整合,而且还有针对性和目的性地对词典进行了去杂、更新。特别地,我们加入了某些行业词汇,以增加分类中的命中率。不同行业某些词语的词频会有比较大的差别,而这些词有可能是情感分类的关键词之一。比如,薛云老师提供的评论数据是有关蒙牛牛奶的,也就是饮食行业的;而在饮食行业中,“吃”和“喝”这两个词出现的频率会相当高,而且通常是对饮食的正面评价,而“不吃”或者“不喝”通常意味着对饮食的否定评价,而在其他行业或领域中,这几个词语则没有明显情感倾向。另外一个例子是手机行业的,比如“这手机很耐摔啊,还防水”,“耐摔”、“防水”就是在手机这个领域有积极情绪的词。因此,有必要将这些因素考虑进模型之中。

    文本情感分类

    基于情感词典的文本情感分类规则比较机械化。简单起见,我们将每个积极情感词语赋予权重1,将每个消极情感词语赋予权重-1,并且假设情感值满足线性叠加原理;然后我们将句子进行分词,如果句子分词后的词语向量包含相应的词语,就加上向前的权值,其中,否定词和程度副词会有特殊的判别规则,否定词会导致权值反号,而程度副词则让权值加倍。最后,根据总权值的正负性来判断句子的情感。基本的算法如图。

    要说明的是,为了编程和测试的可行性,我们作了几个假设(简化)。假设一:我们假设了所有积极词语、消极词语的权重都是相等的,这只是在简单的判断情况下成立,更精准的分类显然不成立的,比如“恨”要比“讨厌”来得严重;修正这个缺陷的方法是给每个词语赋予不同的权值,我们将在本文的第二部分探讨权值的赋予思路。假设二:我们假设了权值是线性叠加的,这在多数情况下都会成立,而在本文的第二部分中,我们会探讨非线性的引入,以增强准确性。假设三:对于否定词和程度副词的处理,我们仅仅是作了简单的取反和加倍,而事实上,各个否定词和程度副词的权值也是不一样的,比如“非常喜欢”显然比“挺喜欢”程度深,但我们对此并没有区分。

    在算法的实现上,我们则选用了Python作为实现平台。可以看到,借助于Python丰富的扩展支持,我们仅用了一百行不到的代码,就实现了以上所有步骤,得到了一个有效的情感分类算法,这充分体现了Python的简洁。下面将检验我们算法的有效性。

    模型结果检验

    作为最基本的检验,我们首先将我们的模型运用于薛云老师提供的蒙牛牛奶评论中,结果是让人满意的,达到了82.02%的正确率,详细的检验报告如下表

    数据内容牛奶评论正样本数1005负样本数1170准确率0.8202真正率0.8209真负率0.8197数据内容正样本数负样本数准确率真正率真负率牛奶评论100511700.82020.82090.8197

    (其中,正样本为积极情感评论,负样本为消极情感数据,

    准确率=被正确判断的样本数总样本数真正率=被判断为积极的正样本数正样本总数真负率=被判断为消极的负样本数负样本总数准确率=被正确判断的样本数总样本数真正率=被判断为积极的正样本数正样本总数真负率=被判断为消极的负样本数负样本总数

    。)

    让我们惊喜的是,将从蒙牛牛奶评论数据中调整出来的模型,直接应用到某款手机的评论数据的情感分类中,也达到了81.96%准确率!这表明我们的模型具有较好的强健性,能在不同行业的评论数据的情感分类中都有不错的表现。

    数据内容手机评论正样本数1158负样本数1159准确率0.8196真正率0.7539真负率0.8852数据内容正样本数负样本数准确率真正率真负率手机评论115811590.81960.75390.8852

    结论:我们队伍初步实现了基于情感词典的文本情感分类,测试结果表明,通过简单的判断规则就能够使这一算法具有不错的准确率,同时具有较好的强健性。一般认为,正确率达80%以上的模型具有一定的生产价值,能适用于工业环境。显然,我们的模型已经初步达到了这个标准。

    困难所在

    经过两次测试,可以初步认为我们的模型正确率基本达到了80%以上。另外,一些比较成熟的商业化程序,它的正确率也只有85%到90%左右(如BosonNLP)。这说明我们这个简单的模型确实已经达到了让人满意的效果,另一方面,该事实也表明,传统的“基于情感词典的文本情感分类”模型的性能可提升幅度相当有限。这是由于文本情感分类的本质复杂性所致的。经过初步的讨论,我们认为文本情感分类的困难在以下几个方面。

    语言系统是相当复杂的

    归根结底,这是因为我们大脑中的语言系统是相当复杂的。(1)我们现在做的是文本情感分类,文本和文本情感都是人类文化的产物,换言之,人是唯一准确的判别标准。(2)人的语言是一个相当复杂的文化产物,一个句子并不是词语的简单线性组合,它有相当复杂的非线性在里面。(3)我们在描述一个句子时,都是将句子作为一个整体而不是词语的集合看待的,词语的不同组合、不同顺序、不同数目都能够带来不同的含义和情感,这导致了文本情感分类工作的困难。

    因此,文本情感分类工作实际上是对人脑思维的模拟。我们前面的模型,实际上已经对此进行了最简单的模拟。然而,我们模拟的不过是一些简单的思维定式,真正的情感判断并不是一些简单的规则,而是一个复杂的网络。

    大脑不仅仅在情感分类

    事实上,我们在判断一个句子的情感时,我们不仅仅在想这个句子是什么情感,而且还会判断这个句子的类型(祈使句、疑问句还是陈述句?);当我们在考虑句子中的每个词语时,我们不仅仅关注其中的积极词语、消极词语、否定词或者程度副词,我们会关注每一个词语(主语、谓语、宾语等等),从而形成对整个句子整体的认识;我们甚至还会联系上下文对句子进行判断。这些判断我们可能是无意识的,但我们大脑确实做了这个事情,以形成对句子的完整认识,才能对句子的感情做了准确的判断。也就是说,我们的大脑实际上是一个非常高速而复杂的处理器,我们要做情感分类,却同时还做了很多事情。

    活水:学习预测

    人类区别于机器、甚至人类区别于其他动物的显著特征,是人类具有学习意识和学习能力。我们获得新知识的途径,除了其他人的传授外,还包括自己的学习、总结和猜测。对于文本情感分类也不例外,我们不仅仅可以记忆住大量的情感词语,同时我们还可以总结或推测出新的情感词语。比如,我们只知道“喜欢”和“爱”都具有积极情感倾向,那么我们会猜测“喜爱”也具有积极的情感色彩。这种学习能力是我们扩充我们的词语的重要方式,也是记忆模式的优化(即我们不需要专门往大脑的语料库中塞进“喜爱”这个词语,我们仅需要记得“喜欢”和“爱”,并赋予它们某种联系,以获得“喜爱”这个词语,这是一种优化的记忆模式)。

    优化思路

    经过上述分析,我们看到了文本情感分类的本质复杂性以及人脑进行分类的几个特征。而针对上述分析,我们提出如下几个改进措施。

    非线性特征的引入

    前面已经提及过,真实的人脑情感分类实际上是严重非线性的,基于简单线性组合的模型性能是有限的。所以为了提高模型的准确率,有必要在模型中引入非线性。

    所谓非线性,指的是词语之间的相互组合形成新的语义。事实上,我们的初步模型中已经简单地引入了非线性——在前面的模型中,我们将积极词语和消极词语相邻的情况,视为一个组合的消极语块,赋予它负的权值。更精细的组合权值可以通过“词典矩阵”来实现,即我们将已知的积极词语和消极词语都放到同一个集合来,然后逐一编号,通过如下的“词典矩阵”,来记录词组的权值。

    词语(空词)喜欢爱⋮讨厌⋮(空词)012⋮−1⋮喜欢123⋮−2⋮爱234⋮−3⋮…………⋮…⋮讨厌−1−2−2⋮−2⋮…………………词语(空词)喜欢爱…讨厌…(空词)012…−1…喜欢123…−2…爱234…−2…⋮⋮⋮⋮⋮⋮…讨厌−1−2−3…−2…⋮⋮⋮⋮⋮⋮…

    并不是每一个词语的组合都是成立的,但我们依然可以计算它们之间的组合权值,情感权值的计算可以阅读参考文献。然而,情感词语的数目相当大,而词典矩阵的元素个数则是其平方,其数据量是相当可观的,因此,这已经初步进入大数据的范畴。为了更加高效地实现非线性,我们需要探索组合词语的优化方案,包括构造方案和储存、索引方案。

    情感词典的自动扩充

    在如今的网络信息时代,新词的出现如雨后春笋,其中包括“新构造网络词语”以及“将已有词语赋予新的含义”;另一方面,我们整理的情感词典中,也不可能完全包含已有的情感词语。因此,自动扩充情感词典是保证情感分类模型时效性的必要条件。目前,通过网络爬虫等手段,我们可以从微博、社区中收集到大量的评论数据,为了从这大批量的数据中找到新的具有情感倾向的词语,我们的思路是无监督学习式的词频统计。

    我们的目标是“自动扩充”,因此我们要达到的目的是基于现有的初步模型来进行无监督学习,完成词典扩充,从而增强模型自身的性能,然后再以同样的方式进行迭代,这是一个正反馈的调节过程。虽然我们可以从网络中大量抓取评论数据,但是这些数据是无标注的,我们要通过已有的模型对评论数据进行情感分类,然后在同一类情感(积极或消极)的评论集合中统计各个词语的出现频率,最后将积极、消极评论集的各个词语的词频进行对比。某个词语在积极评论集中的词频相当高,在消极评论集中的词频相当低,那么我们就有把握将该词语添加到消极情感词典中,或者说,赋予该词语负的权值。

    举例来说,假设我们的消极情感词典中并没有“黑心”这个词语,但是“可恶”、“讨厌”、“反感”、“喜欢”等基本的情感词语在情感词典中已经存在,那么我们就会能够将下述句子正确地进行情感分类:

    句子这个黑心老板太可恶了我很反感这黑心企业的做法很讨厌这家黑心店铺这家店铺真黑心!⋮权值−2−2−20⋮句子权值这个黑心老板太可恶了−2我很反感这黑心企业的做法−2很讨厌这家黑心店铺−2这家店铺真黑心!0⋮⋮

    其中,由于消极情感词典中没有“黑心”这个词语,所以“这家店铺真黑心!”就只会被判断为中性(即权值为0)。分类完成后,对所有词频为正和为负的分别统计各个词频,我们发现,新词语“黑心”在负面评论中出现很多次,但是在正面评论中几乎没有出现,那么我们就将黑心这个词语添加到我们的消极情感词典中,然后更新我们的分类结果:

    句子这个黑心老板太可恶了我很反感这黑心企业的做法很讨厌这家黑心店铺这家店铺真黑心!⋮权值−3−3−3−2⋮句子权值这个黑心老板太可恶了−3我很反感这黑心企业的做法−3很讨厌这家黑心店铺−3这家店铺真黑心!−2⋮⋮

    于是我们就通过无监督式的学习扩充了词典,同时提高了准确率,增强了模型的性能。这是一个反复迭代的过程,前一步的结果可以帮助后一步的进行。

    本文结论

    综合上述研究,我们得出如下结论:

    基于情感词典的文本情感分类是容易实现的,其核心之处在于情感词典的训练。

    语言系统是相当复杂的,基于情感词典的文本情感分类只是一个线性的模型,其性能是有限的。

    在文本情感分类中适当地引入非线性特征,能够有效地提高模型的准确率。

    引入扩充词典的无监督学习机制,可以有效地发现新的情感词,保证模型的强健性和时效性。

    参考文献

    实现平台

    我们队所做的编程工具,在以下环境中测试完成:

    Windows 8.1 微软操作系统。

    Python 3.4 开发平台/编程语言。选择3.x而不是2.x版本的主要原因是3.x版本对中文字符的支持更好。

    Numpy Python的一个数值计算库,为Python提供了快速的多维数组处理的能力。

    Pandas Python的一个数据分析包。

    结巴分词 Python平台的一个中文分词工具,也有Java、C++、Node.js等版本。

    代码列表

    预处理

    加载情感词典

    预测函数

    简单的测试

    转载到请包括本文地址:如果您觉得本文还不错,欢迎点击下面的按钮对博主进行打赏。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!

    展开全文
  • kesci文本情感分类练习赛-附件资源
  • 对于一个简单的文本情感分类来说,其实就是一个二分类,这篇博客主要讲述的是使用scikit-learn来做文本情感分类。分类主要分为两步:1)训练,主要根据训练集来学习分类模型的规则。2)分类,先用已知的测试集评估分类...

    对于一个简单的文本情感分类来说,其实就是一个二分类,这篇博客主要讲述的是使用scikit-learn来做文本情感分类。分类主要分为两步:1)训练,主要根据训练集来学习分类模型的规则。2)分类,先用已知的测试集评估分类的准确率等,如果效果还可以,那么该模型对无标注的待测样本进行预测。

    首先先介绍下我样本集,样本是已经分好词的酒店评论,第一列为标签,第二列为评论,前半部分为积极评论,后半部分为消极评论,格式如下:

    下面实现了SVM,NB,逻辑回归,决策树,逻辑森林,KNN 等几种分类方法,主要代码如下:

    #coding:utf-8

    from matplotlib import pyplot

    import scipy as sp

    import numpy as np

    from sklearn.cross_validation import train_test_split

    from sklearn.feature_extraction.text import CountVectorizer

    from sklearn.feature_extraction.text import TfidfVectorizer

    from sklearn.metrics import precision_recall_curve

    from sklearn.metrics import classification_report

    from numpy import *

    #========SVM========#

    def SvmClass(x_train, y_train):

    from sklearn.svm import SVC

    #调分类器

    clf = SVC(kernel = 'linear',probability=True)#default with 'rbf'

    clf.fit(x_train, y_train)#训练,对于监督模型来说是 fit(X, y),对于非监督模型是 fit(X)

    return clf

    #=====NB=========#

    def NbClass(x_train, y_train):

    from sklearn.naive_bayes import MultinomialNB

    clf=MultinomialNB(alpha=0.01).fit(x_train, y_train)

    return clf

    #========Logistic Regression========#

    def LogisticClass(x_train, y_train):

    from sklearn.linear_model import LogisticRegression

    clf = LogisticRegression(penalty='l2')

    clf.fit(x_train, y_train)

    return clf

    #========KNN========#

    def KnnClass(x_train,y_train):

    from sklearn.neighbors import KNeighborsClassifier

    clf=KNeighborsClassifier()

    clf.fit(x_train,y_train)

    return clf

    #========Decision Tree ========#

    def DccisionClass(x_train,y_train):

    from sklearn import tree

    clf=tree.DecisionTreeClassifier()

    clf.fit(x_train,y_train)

    return clf

    #========Random Forest Classifier ========#

    def random_forest_class(x_train,y_train):

    from sklearn.ensemble import RandomForestClassifier

    clf= RandomForestClassifier(n_estimators=8)#参数n_estimators设置弱分类器的数量

    clf.fit(x_train,y_train)

    return clf

    #========准确率召回率 ========#

    def Precision(clf):

    doc_class_predicted = clf.predict(x_test)

    print(np.mean(doc_class_predicted == y_test))#预测结果和真实标签

    #准确率与召回率

    precision, recall, thresholds = precision_recall_curve(y_test, clf.predict(x_test))

    answer = clf.predict_proba(x_test)[:,1]

    report = answer > 0.5

    print(classification_report(y_test, report, target_names = ['neg', 'pos']))

    print("--------------------")

    from sklearn.metrics import accuracy_score

    print('准确率: %.2f' % accuracy_score(y_test, doc_class_predicted))

    if __name__ == '__main__':

    data=[]

    labels=[]

    with open ("train2.txt","r")as file:

    for line in file:

    line=line[0:1]

    labels.append(line)

    with open("train2.txt","r")as file:

    for line in file:

    line=line[1:]

    data.append(line)

    x=np.array(data)

    labels=np.array(labels)

    labels=[int (i)for i in labels]

    movie_target=labels

    #转换成空间向量

    count_vec = TfidfVectorizer(binary = False)

    #加载数据集,切分数据集80%训练,20%测试

    x_train, x_test, y_train, y_test= train_test_split(x, movie_target, test_size = 0.2)

    x_train = count_vec.fit_transform(x_train)

    x_test = count_vec.transform(x_test)

    print('**************支持向量机************ ')

    Precision(SvmClass(x_train, y_train))

    print('**************朴素贝叶斯************ ')

    Precision(NbClass(x_train, y_train))

    print('**************最近邻KNN************ ')

    Precision(KnnClass(x_train,y_train))

    print('**************逻辑回归************ ')

    Precision(LogisticClass(x_train, y_train))

    print('**************决策树************ ')

    Precision(DccisionClass(x_train,y_train))

    print('**************逻辑森林************ ')

    Precision(random_forest_class(x_train,y_train))

    结果如下:

    2766967eb6ac5a34bfc88e2028aaebd5.png

    对于整体代码和语料的下载,可以去下载

    展开全文
  • 基于情感词典的文本情感分类

    千次阅读 2017-08-29 09:36:50
    基于情感词典的文本情感分类 传统的基于情感词典的文本情感分类,是对人的记忆和判断思维的最简单的模拟,如上图。我们首先通过学习来记忆一些基本词汇,如否定词语有“不”,积极词语有“喜欢”、“爱”,消极...
  • Web文本情感分类研究综述 Web文本情感分类研究综述Web文本情感分类研究综述Web文本情感分类研究综述
  • pytorch-文本情感分类

    千次阅读 2020-02-25 10:28:42
    文本情感分类 文本分类是自然语言处理的一个常见任务,它把一段不定长的文本序列变换为文本的类别。本节关注它的一个子问题:使用文本情感分类来分析文本作者的情绪。这个问题也叫情感分析,并有着广泛的应用。 同...
  • 基于改进特征选择方法的文本情感分类研究
  • 用AllenNLP/PyTorch训练文本情感分类
  • 电影文本情感分类

    千次阅读 2018-05-11 10:17:23
    电影文本情感分类Github地址Kaggle地址这个任务主要是对电影评论文本进行情感分类,主要分为正面评论和负面评论,所以是一个二分类问题,二分类模型我们可以选取一些常见的模型比如贝叶斯、逻辑回归等,这里挑战之一...
  • python 文本情感分类

    千次阅读 2017-06-21 19:54:50
    对于一个简单的文本情感分类来说,其实就是一个二分类,这篇博客主要讲述的是使用scikit-learn来做文本情感分类。分类主要分为两步:1)训练,主要根据训练集来学习分类模型的规则。2)分类,先用已知的测试集评估...
  • 基于双向时间深度卷积网络的中文文本情感分类
  • 基于情感词典的文本情感分类传统的基于情感词典的文本情感分类,是对人的记忆和判断思维的最简单的模拟,如上图。我们首先通过学习来记忆一些基本词汇,如否定词语有“不”,积极词语有“喜欢”、“爱”,消极词语有...
  • 原标题:基于Python的文本情感分类前言在上一期《【干货】--手把手教你完成文本情感分类》中我们使用了R语言对酒店评论数据做了情感分类,基于网友的需求,这里再使用Python做一下复现。关于步骤、理论部分这里就...
  • 中文文本情感分类及情感分析资源大全

    万次阅读 多人点赞 2018-10-03 22:43:49
    本文主要是基于机器学习方法的中文文本情感分类,主要包括:使用开源的Markup处理程序对XML文件进行分析处理、中科院计算所开源的中文分词处理程序ICTCLAS对文本进行分词处理、去除停用词等文本预处理,在基于向量...
  • 一种基于情感词典和朴素贝叶斯的中文文本情感分类方法
  • 基于机器学习的文本情感分类研究 详细算法文档,pdf格式,高清扫描版 >120页,内容比较详细,需要一定理论功底。
  • 基于BRC的不平衡文本情感分类的样本切割方法
  • 一种基于情感句模的文本情感分类方法,陈涛,徐睿峰,考虑到同类型的情感句往往具有相同或者相似的句法和语义表达模式,本文提出了一种基于情感句模的文本情感自动分类方法。首先,将
  • 基于朴素贝叶斯理论提出了一种新的中文文本情感分类方法。这种方法利用情感词典对文本进行处理和表示,基于朴素贝叶斯理论构建文本情感分类器,并以互联网上宾馆中文评论作为分类研究的对象。实验表明,使用提出的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 2,257
精华内容 902
关键字:

文本情感分类