我们的文本分类系统使用的是THUCnews数据集。
目录
1. 数据集简介
THUCNews是根据新浪新闻RSS订阅频道2005~2011年间的历史数据筛选过滤生成,包含74万篇新闻文档(2.19 GB),均为UTF-8纯文本格式。我们在原始新浪新闻分类体系的基础上,重新整合划分出14个候选分类类别:财经、彩票、房产、股票、家居、教育、科技、社会、时尚、时政、体育、星座、游戏、娱乐。
2. 数据预处理
在进行特征提取之前,需要对原始文本数据进行预处理,这对于特征提取来说至关重要,一个好的预处理过程会显著的提高特征提取的质量以及分类算法的性能。 文本预处理一般包括以下步骤:
(1)分词:首先,需要把文本切分成单词或短语。对于英文文本,可以直接按照空格进行切分(此时句末的标点不会单独切分出来)或使用一些英文分词工具如 nltk中的分词工具; 对于中文文本,可以使用分词工具(如 jieba 等)进行切分。(文本分类算法有基于词和基于字符两种处理方式,一般来说基于词的文本分类算法效果更好,本专栏介绍的文本分类算法都是基于词的处理方式)。
(2)去停止词:所谓停止词,就是在文本中大量出现但对分类并没有太多作用的词。如英文里的{'a','an','the','above','after','of'......}中文里的{'的','这','那',...}在这一步,把文本中的停止词过滤掉。(英文文本去停止词,可以直接使用nltk中封装的函数,中文文本可以自行下载停用词表(我们使用的是哈工大停用词表),来进行过滤)。
(3)小写化:这一步主要针对英文文本,大多数情况下需要把英文文本统一转换为小写形式。
(4)噪声移除:去除文本中的特殊符号,如特殊标点等。这些特殊符号对于人类理解文本可能很重要,但对于分类算法并没有太大意义(可以和停止词一并去除)。
(5)拼写检查: 数据集可能来自一些社交媒体,存在一些拼写错误,可以尝试对拼写问题进行纠正。
(6)俚语和缩写:可以尝试把缩写还原为完整形式或将一些较口语化的表示转换为书面语。
(7)词干提取和词型还原:这一步主要针对英文文本,即将单词转换为最基本的形式。如 studying → study、apples → apple 等。
(8)词频统计与过滤:对文本训练集进行上述处理后,统计剩余单词的词频, 并过滤低频词(可以设置一个阈值(如,5),保留词频大于阈值的单词;或者基于词频进行排序,取前k个词。前者词典的大小不确定,后者词典大小是确定的,就是k(或k+2包括位置符号unk和填充符号pad))。对剩余单词,构建词典。
3. 具体细节
- 读取数据
def read_cnews():
#opt.data_root为数据集解压后 文件夹所在路径 定义在config.py中
data = []
#所有的主题标签
labels = [label for label in os.listdir(opt.data_root) if label != '.DS_Store']
print(labels)
#标签到整数索引的映射
labels2index = dict(zip(labels, list(range(len(labels)))))
#整数索引到标签的映射
index2labels = dict(zip(list(range(len(labels))),labels))
print(labels2index)
print(index2labels)
#存储整数索引到标签的映射 以便预测时使用
with open('index2labels.json','w') as f:
json.dump(index2labels,f)
#存储类别标签 打印分类报告会用到
with open('labels.json','w') as f:
json.dump(labels,f)
for label in labels:
folder_name = os.path.join(opt.data_root,label)
datasub = [] #存储某一类的数据 [[string,index],...]
for file in tqdm(os.listdir(folder_name)):
with open(os.path.join(folder_name, file), 'rb') as f:
#去除文本中空白符
review = f.read().decode('utf-8').replace('\n', '').replace('\r','').replace('\t','')
datasub.append([review,labels2index[label]])
data.append(datasub) #存储所有类的数据[[[string,index],...],[[string,index],...],...]
return data
- 切分训练集、验证集和测试集
def split_data(data):
#切分数据集 为训练集、验证集和测试集
train_data = []
val_data = []
test_data = []
#对每一类数据进行打乱
#设置验证集和测试集中每一类样本数都为200(样本均衡)
for data1 in data: #遍历每一类数据
np.random.shuffle(data1) #打乱
val_data += data1[:200]
test_data += data1[200:400]
train_data += data1[400:]
np.random.shuffle(train_data) #打乱训练集 测试机和验证集不用打乱
print(len(train_data))
print(len(val_data))
print(len(test_data))
return train_data,val_data,test_data
- 基于训练集构建词典
#读取停用词
def stopwords(fileroot):
#fileroot为下载的停用词表所在的路径
with open(fileroot,'r') as f:
stopword = [line.strip() for line in f]
print(stopword[:5])
return stopword
#分词 去除停用词
def get_tokenized(data,stopword):
"""
data: list of [string, label]
"""
def tokenizer(text):
return [tok for tok in jieba.lcut(text) if tok not in stopword]
return [tokenizer(review) for review, _ in data]
#构建词典
def get_vocab(data,stopword):
tokenized_data = get_tokenized(data,stopword) #分词、去除停用词
counter = collections.Counter([tk for st in tokenized_data for tk in st]) #统计词频
return Vocab.Vocab(counter, min_freq=5,specials=['<pad>','<unk>']) #保留词频大于5的词 <pad>对应填充项(词典中第0个词) <unk>对应低频词和停止词等未知词(词典中第1个词)
- 基于词典对训练集、验证集以及测试集进行处理
def preprocess_imdb(data, vocab,stopword):
#将训练集、验证集、测试集中单词转换为词典中对应的索引
max_l = 500 # 将每条新闻通过截断或者补0,使得长度变成500(所有数据统一成一个长度,方便用矩阵并行计算(其实也可以每个 batch填充成一个长度,batch间可以不一样))
def pad(x):
return x[:max_l] if len(x) > max_l else x + [0] * (max_l - len(x))
tokenized_data = get_tokenized(data,stopword) #分词、去停止词
features = torch.tensor([pad([vocab.stoi[word] for word in words]) for words in tokenized_data]) #把单词转换为词典中对应的索引,并填充成固定长度,封装为tensor
labels = torch.tensor([score for _, score in data])
return features, labels
- 保存一些中间结果
with open('word2index.json','w') as f: #保存词到索引的映射,预测和后续加载预训练词向量时会用到
json.dump(vocab.stoi,f)
with open('vocabsize.json','w') as f: #保存词典的大小(因为我们基于词频阈值过滤低频词,词典大小不确定,需要保存,后续模型中会用到)
json.dump(len(vocab),f)
#保存预处理好的训练集、验证集和测试集 以便后续训练时使用
torch.save(X_train,'X_train.pt')
torch.save(y_train, 'y_train.pt')
torch.save(X_val, 'X_val.pt')
torch.save(y_val, 'y_val.pt')
torch.save(X_test, 'X_test.pt')
torch.save(y_test, 'y_test.pt')