使用pytorch和torchtext进行文本分类的实例


Posted in Python onJanuary 08, 2020

文本分类是NLP领域的较为容易的入门问题,本文记录我自己在做文本分类任务以及复现相关论文时的基本流程,绝大部分操作都使用了torch和torchtext两个库。

1. 文本数据预处理

首先数据存储在三个csv文件中,分别是train.csv,valid.csv,test.csv,第一列存储的是文本数据,例如情感分类问题经常是用户的评论review,例如imdb或者amazon数据集。第二列是情感极性polarity,N分类问题的话就有N个值,假设值得范围是0~N-1。

下面是很常见的文本预处理流程,英文文本的话不需要分词,直接按空格split就行了,这里只会主要说说第4点。

1、去除非文本部分

2、分词

3、去除停用词

4、对英文单词进行词干提取(stemming)和词型还原(lemmatization)

5、转为小写

6、特征处理

Bag of Words

Tf-idf

N-gram

Word2vec

词干提取和词型还原

from nltk.stem import SnowballStemmer
stemmer = SnowballStemmer("english") # 选择语言
from nltk.stem import WordNetLemmatizer 
wnl = WordNetLemmatizer()

SnowballStemmer较为激进,转换有可能出现错误,这里较为推荐使用WordNetLemmatizer,它一般只在非常肯定的情况下才进行转换,否则会返回原来的单词。

stemmer.stem('knives')
# knive
wnl.lemmatize('knives')
# knife

因为我没有系统学习和研究过NLTK的代码,所以就不多说了,有兴趣的可以自己去阅读NLTK的源码。

2. 使用torchtext加载文本数据

本节主要是用的模块是torchtext里的data模块,处理的数据同上一节所描述。

首先定义一个tokenizer用来处理文本,比如分词,小写化,如果你已经根据上一节的词干提取和词型还原的方法处理过文本里的每一个单词后可以直接分词就够了。

tokenize = lambda x: x.split()

或者也可以更保险点,使用spacy库,不过就肯定更耗费时间了。

import spacy

spacy_en = spacy.load('en')
def tokenizer(text):
 return [toke.text for toke in spacy_en.tokenizer(text)]

然后要定义Field,至于Field是啥,你可以简单地把它理解为一个能够加载、预处理和存储文本数据和标签的对象。我们可以用它根据训练数据来建立词表,加载预训练的Glove词向量等等。

def DataLoader():
 tokenize = lambda x: x.split()
 # 用户评论,include_lengths设为True是为了方便之后使用torch的pack_padded_sequence
 REVIEW = data.Field(sequential=True,tokenize=tokenize, include_lengths=True)
 # 情感极性
 POLARITY = data.LabelField(sequential=False, use_vocab=False, dtype = torch.long)
 # 假如train.csv文件并不是只有两列,比如1、3列是review和polarity,2列是我们不需要的数据,
 # 那么就要添加一个全是None的元组, fields列表存储的Field的顺序必须和csv文件中每一列的顺序对应,
 # 否则review可能就加载到polarity Field里去了
 fields = [('review', REVIEW), (None, None), ('polarity', POLARITY)]
 
 # 加载train,valid,test数据
 train_data, valid_data, test_data = data.TabularDataset.splits(
         path = 'amazon',
         train = 'train.csv',
      validation = 'valid.csv',
      test = 'test.csv',
         format = 'csv',
         fields = fields,
         skip_header = False # 是否跳过文件的第一行
 )
 return REVIEW, POLARITY, train_data

加载完数据可以开始建词表。如果本地没有预训练的词向量文件,在运行下面的代码时会自动下载到当前文件夹下的'.vector_cache'文件夹内,如果本地已经下好了,可以用Vectors指定文件名name,路径cache,还可以使用Glove。

from torchtext.vocab import Vectors, Glove
import torch

REVIEW, POLARITY, train_data = DataLoader()
# vectors = Vectors(name='glove.6B.300d.txt', cache='.vector_cache')
REVIEW.build_vocab(train_data, # 建词表是用训练集建,不要用验证集和测试集
     max_size=400000, # 单词表容量
     vectors='glove.6B.300d', # 还有'glove.840B.300d'已经很多可以选
     unk_init=torch.Tensor.normal_ # 初始化train_data中不存在预训练词向量词表中的单词
)

# print(REVIEW.vocab.freqs.most_common(20)) 数据集里最常出现的20个单词
# print(REVIEW.vocab.itos[:10])  列表 index to word
# print(REVIEW.vocab.stoi)    字典 word to index

接着就是把预训练词向量加载到model的embedding weight里去了。

pretrained_embeddings = REVIEW.vocab.vectors
model.embedding.weight.data.copy_(pretrained_embeddings)
UNK_IDX = REVIEW.vocab.stoi[REVIEW.unk_token]
PAD_IDX = REVIEW.vocab.stoi[REVIEW.pad_token]
# 因为预训练的权重的unk和pad的词向量不是在我们的数据集语料上训练得到的,所以最好置零
model.embedding.weight.data[UNK_IDX] = torch.zeros(EMBEDDING_DIM)
model.embedding.weight.data[PAD_IDX] = torch.zeros(EMBEDDING_DIM)

然后用torchtext的迭代器来批量加载数据,torchtext.data里的BucketIterator非常好用,它可以把长度相近的文本数据尽量都放到一个batch里,这样最大程度地减少padding,数据就少了很多无意义的0,也减少了矩阵计算量,也许还能对最终准确度有帮助(误)?我凭直觉猜的,没有做实验对比过,但是至少能加速训练迭代应该是没有疑问的,如果哪天我有钱了买了台好点的服务器做完实验再来补充。

sort_within_batch设为True的话,一个batch内的数据就会按sort_key的排列规则降序排列,sort_key是排列的规则,这里使用的是review的长度,即每条用户评论所包含的单词数量。

train_iterator, valid_iterator, test_iterator = data.BucketIterator.splits(
            (train_data, valid_data, test_data),
            batch_size=32,
            sort_within_batch=True,
            sort_key = lambda x:len(x.review),
            device=torch.device('cpu'))

最后就是加载数据喂给模型了。

for batch in train_iterator:
 # 因为REVIEW Field的inclue_lengths为True,所以还会包含一个句子长度的Tensor
 review, review_len = batch.review 
 # review.size = (seq_length, batch_size) , review_len.size = (batch_size, )
 polarity = batch.polarity
 # polarity.size = (batch_size, )
 predictions = model(review, review_lengths)
 loss = criterion(predictions, polarity) # criterion = nn.CrossEntropyLoss()

3. 使用pytorch写一个LSTM情感分类器

下面是我简略写的一个模型,仅供参考

import torch.nn as nn
import torch.nn.functional as F
from torch.nn.utils.rnn import pack_padded_sequence
import torch


class LSTM(nn.Module):

 def __init__(self, vocab_size, embedding_dim, hidden_dim, output_dim,
     n_layers, bidirectional, dropout, pad_idx):
  super(LSTM, self).__init__()
  self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=pad_idx)
  self.lstm = nn.LSTM(embedding_dim, hidden_dim, num_layers=n_layers,
       bidirectional=bidirectional, dropout=dropout)
  self.Ws = nn.Parameter(torch.Tensor(hidden_dim, output_dim))
  self.bs = nn.Parameter(torch.zeros((output_dim, )))
  nn.init.uniform_(self.Ws, -0.1, 0.1)
  nn.init.uniform_(self.bs, -0.1, 0.1)
  self.dropout = nn.Dropout(p=0.5)

 def forward(self, x, x_len):
  x = self.embedding(x)
  x = pack_padded_sequence(x, x_len)
  H, (h_n, c_n) = self.lstm(x)
  h_n = self.dropout(h_n)
  h_n = torch.squeeze(h_n)
  res = torch.matmul(h_n, self.Ws) + self.bs
  y = F.softmax(res, dim=1)
  # y.size(batch_size, output_dim)
  return y

训练函数

def train(model, iterator, optimizer, criterion):
 epoch_loss = 0
 num_sample = 0
 correct = 0

 model.train()
 for batch in iterator:
  optimizer.zero_grad()
  review, review_lengths = batch.review
  polarity = batch.polarity
  predictions = model(review, review_lengths)
  correct += torch.sum(torch.argmax(preds, dim=1) == polarity)
  loss = criterion(predictions, polarity)
  loss.backward()
  epoch_loss += loss.item()
  num_sample += len(batch)
  optimizer.step()

 return epoch_loss / num_sample, correct.float() / num_sample

if __name__ == '__main__':
 for epoch in range(N_EPOCHS):
 train_loss, acc = train(model, train_iter, optimizer, criterion)
 print(f'\tTrain Loss: {train_loss:.3f} | Train Acc: {acc* 100:.2f}%')

注意事项和遇到的一些坑

文本情感分类需不需要去除停用词?

应该是不用的,否则acc有可能下降。

data.TabularDataset.splits虽然好用,但是如果你只想加载训练集,这时候如果直接不给validation和test参数赋值,那么其他代码和原来一样,比如这样

train_data = data.TabularDataset.splits(
         path = '',
         train = 'train.csv',
         format = 'csv',
         fields = fields,
         skip_header = False # 是否跳过文件的第一行
)

那么底下你一定会报错,因为data.TabularDataset.splits返回的是一个元组,也就是如果是训练验证测试三个文件都给了函数,就返回(train_data, valid_data, test_data),这时候你用三个变量去接受函数返回值当然没问题,元组会自动拆包。

当只给函数一个文件train.csv时,函数返回的是(train_data)而非train_data,因此正确的写法应该如下

train_data = data.TabularDataset.splits(
         path = '',
         train = 'train.csv',
         format = 'csv',
         fields = fields,
         skip_header = False # 是否跳过文件的第一行
)[0] # 注意这里的切片,选择元组的第一个也是唯一一个元素赋给train_data

同理data.BucketIterator.splits也有相同的问题,它不但返回的是元组,它的参数datasets要求也是以元组形式,即(train_data, valid_data, test_data)进行赋值,否则在下面的运行中也会出现各种各样奇怪的问题。

如果你要生成两个及以上的迭代器,那么没问题,直接照上面写就完事了。

如果你只要生成train_iterator,那么正确的写法应该是下面这样

train_iter = data.BucketIterator(
   train_data,
   batch_size=32,
   sort_key=lambda x:len(x.review),
   sort_within_batch=True,
   shuffle=True # 训练集需要shuffle,但因为验证测试集不需要
    # 可以生成验证和测试集的迭代器直接用data.iterator.Iterator类就足够了
)

出现的问题 x = pack_padded_sequence(x, x_len) 当数据集有长度为0的句子时, 就会后面报错

Adagrad效果比Adam好的多

4. 总结

不仅仅是NLP领域,在各大顶会中,越来越多的学者选择使用Pytorch而非TensorFlow,主要原因就是因为它的易用性,torchtext和pytorch搭配起来是非常方便的NLP工具,可以大大缩短文本预处理,加载数据的时间。

我本人之前用过tf 1.x以及keras,最终拥抱了Pytorch,也是因为它与Numpy极其类似的用法,更Pythonic的代码,清晰的源码让我在遇到bug时能一步一步找到问题所在,动态图让人能随时看到输出的Tensor的全部信息,这些都是Pytorch的优势。

现在tf 2.0也在不断改进,有人笑称tf越来越像pytorch了,其实pytorch也在不断向tf学习,在工业界,tf仍然处于王者地位,不知道未来pytorch能不能在工业界也与tf平分秋色,甚至更胜一筹呢?

以上这篇使用pytorch和torchtext进行文本分类的实例就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持三水点靠木。

Python 相关文章推荐
Python Mysql数据库操作 Perl操作Mysql数据库
Jan 12 Python
Python实现的tab文件操作类分享
Nov 20 Python
Python中使用Inotify监控文件实例
Feb 14 Python
python简单猜数游戏实例
Jul 09 Python
使用python根据端口号关闭进程的方法
Nov 06 Python
Python装饰器基础概念与用法详解
Dec 22 Python
python2爬取百度贴吧指定关键字和图片代码实例
Aug 14 Python
python的移位操作实现详解
Aug 21 Python
Django restframework 框架认证、权限、限流用法示例
Dec 21 Python
pytorch数据预处理错误的解决
Feb 20 Python
Python轻量级web框架bottle使用方法解析
Jun 13 Python
Python预测2020高考分数和录取情况
Jul 08 Python
python爬虫爬取监控教务系统的思路详解
Jan 08 #Python
Pytorch实现基于CharRNN的文本分类与生成示例
Jan 08 #Python
python实现单目标、多目标、多尺度、自定义特征的KCF跟踪算法(实例代码)
Jan 08 #Python
Pytorch实现神经网络的分类方式
Jan 08 #Python
python 爬取古诗文存入mysql数据库的方法
Jan 08 #Python
基于python3抓取pinpoint应用信息入库
Jan 08 #Python
Python PyInstaller安装和使用教程详解
Jan 08 #Python
You might like
ThinkPHP结合ajax、Mysql实现的客户端通信功能代码示例
2014/06/23 PHP
smarty模板判断数组为空的方法
2015/06/10 PHP
PHP文件上传之多文件上传的实现思路
2016/01/27 PHP
在PHP语言中使用JSON和将json还原成数组的方法
2016/07/19 PHP
DOM 基本方法
2009/07/18 Javascript
javascript替换已有元素replaceChild()使用介绍
2014/04/03 Javascript
jQuery .tmpl() 用法示例介绍
2014/08/21 Javascript
js脚本实现数据去重
2014/11/27 Javascript
js实现checkbox全选、不选与反选的方法
2015/02/09 Javascript
javascript生成大小写字母
2015/07/03 Javascript
js弹出窗口返回值的简单实例
2016/05/28 Javascript
浅谈Angularjs link和compile的使用区别
2016/10/21 Javascript
JS中判断null的方法分析
2016/11/21 Javascript
详解nodejs微信公众号开发——5.素材管理接口
2017/04/11 NodeJs
angular directive的简单使用总结
2017/05/24 Javascript
详解JS中统计函数执行次数与执行时间
2018/09/04 Javascript
vue1.0和vue2.0的watch监听事件写法详解
2018/09/11 Javascript
[02:02]DOTA2英雄基础教程 斯拉达
2013/12/11 DOTA
使用python实现正则匹配检索远端FTP目录下的文件
2015/03/25 Python
浅谈python正则的常用方法 覆盖范围70%以上
2018/03/14 Python
python实现将文件夹内的每张图片批量分割成多张
2019/07/22 Python
在python中用url_for构造URL的方法
2019/07/25 Python
Python 硬币兑换问题
2019/07/29 Python
python接口调用已训练好的caffe模型测试分类方法
2019/08/26 Python
Python numpy数组转置与轴变换
2019/11/15 Python
python 实现在shell窗口中编写print不向屏幕输出
2020/02/19 Python
Python内置函数locals和globals对比
2020/04/28 Python
Django用户登录与注册系统的实现示例
2020/06/03 Python
CSS 3.0 结合video视频实现的创意开幕效果
2020/06/01 HTML / CSS
美国知名的在线旅游服务网站:Priceline
2016/07/23 全球购物
英国男女奢华内衣和泳装购物网站:Figleaves
2017/01/28 全球购物
英国皇家邮政海外旗舰店:Royal Mail
2018/02/21 全球购物
自动化系在校本科生求职信
2013/10/23 职场文书
寄语十八大感言
2014/02/07 职场文书
放飞中国梦演讲稿
2014/04/23 职场文书
运动员入场词
2015/07/18 职场文书