本文代码参考:
https://github.com/percent4/pytorch_transformer_chinese_text_classification
1. 数据集预处理
- 主要通过对数据集的预处理得到字库与标签库,并把字库与标签分表序列化存储到文件。
- 标签存储文件:
labels.pk
- 字库存储文件:
chars.pk
- 标签存储文件:
import os
import pickle
import pandas as pd
from random import shuffle
from operator import itemgetter
from collections import Counter, defaultdict
1.1. 文件保存的封装实现
# pickle文件操作
class PickleFileOperator:
def __init__(self, data=None, file_path=''):
self.data = data
self.file_path = file_path
def save(self):
with open(self.file_path, 'wb') as f:
pickle.dump(self.data, f)
def read(self):
with open(self.file_path, "rb") as f:
content = pickle.load(f)
return content
1.2. 预处理数据集
- 通过对数据集的统计得到标签库与字库。
(1)数据集预处理参数
# 数据集文件
DATASETS_DIR = "./datasets"
TRAIN_FILE_PATH = os.path.join(DATASETS_DIR, 'train.csv') # 训练数据集
TEST_FILE_PATH = os.path.join(DATASETS_DIR, 'test.csv') # 测试数据集
# 字库的最大数量
NUM_WORDS = 5500 # 用来限制字库的最大容量,
(2)数据集预处理
- 打开数据集,读取标签与内容。
- 对标签唯一化处理,得到标签库
- 对内容,统计得到字库,字库是打乱后,随机取
NUM_WORDS
大的数量。这个操作是认为产生一些不在统计范围的字,降低模型训练的拟合性。
class FilePreprossing(object):
def __init__(self, n):
# 保留前n个高频字
self.__n = n
def _read_train_file(self):
train_pd = pd.read_csv(TRAIN_FILE_PATH)
label_list = train_pd['label'].unique().tolist()
# 统计文字频数
character_dict = defaultdict(int)
for content in train_pd['content']:
for key, value in Counter(content).items():
character_dict[key] += value
# 不排序
sort_char_list = [(k, v) for k, v in character_dict.items()]
shuffle(sort_char_list)
# 排序
# sort_char_list = sorted(character_dict.items(), key=itemgetter(1), reverse=True)
print(f'数据集共计 {len(character_dict)} 汉字.')
print('随机打乱后,前10个字的统计: ', sort_char_list[:10])
# 保留前n个文字
top_n_chars = [_[0] for _ in sort_char_list[:self.__n]] # 这里只保留了前n个字。(注意:这里对汉字没有采用分词处理,而是直接处理字)
print("最终字库的总数:", len(top_n_chars))
return label_list, top_n_chars
def run(self):
label_list, top_n_chars = self._read_train_file()
PickleFileOperator(data=label_list, file_path='labels.pk').save()
PickleFileOperator(data=top_n_chars, file_path='chars.pk').save()
(3)执行数据集预处理
processor = FilePreprossing(NUM_WORDS)
processor.run()
数据集共计 5259 汉字.
随机打乱后,前10个字的统计: [('蛎', 3), ('娼', 9), ('Ⅱ', 43), ('识', 2534), ('钓', 36), ('座', 825), ('晁', 6), ('按', 1800), ('迢', 2), ('伎', 8)]
最终字库的总数: 5259
labels = PickleFileOperator(file_path='labels.pk').read()
print("标签:", labels)
content = PickleFileOperator(file_path='chars.pk').read()
print("字库的前10个字(随机打乱的,没有按照统计数量排序):", content[:10])
标签: ['体育', '健康', '军事', '教育', '汽车']
字库的前10个字(随机打乱的,没有按照统计数量排序): ['蛎', '娼', 'Ⅱ', '识', '钓', '座', '晁', '按', '迢', '伎']
2. 数据集特征处理(数据集工程)
- 把数据集处理成在模型可以使用的格式:向量。
- 模型训练
- 模型测试
- 模型评估
- 模型推理
- 在PyTorch中需要处理成Dataset与DataLoader。
- Dataset是数据集格式。
- DataLoader是批次数据集格式。
- 数据集特征处理主要是文本向量化,向量化技术很多,这里采用的向量化方式:
- 对字库编号,使用编号代替字,实现数值化,
- 文本字符串就可以轻松转化为向量。
- 为了保证向量的维度形状一致,对每个句子都进行了对齐处理:
- 指定一个对齐长度SENT_LENGTH
- 大于SENT_LENGTH的句子截断处理
- 小于SENT_LENGTH长度的进行补齐,补齐的字符统一采用PAD定义字符替代,补齐字符的编号采用PAD_NO定义的编号,一般用0
- 如果碰见字库中没有的字,则使用UNK定义的字符替代。UNK的字符编号采用UNK_NO定义的编号,一般使用1。
import pandas as pd
import numpy as np
import torch as T
from torch.utils.data import Dataset, random_split
2.1. 读取标签库与字库
- 把上面与处理的字库与标签数值化:
- 使用顺序编号,把编号与字与标签对应起来。
- 采用字典存储标签与编码的对应关系,存储字与编号对应关系。
- 后面可以通过字典把预测的编号还原为字与文本标签。
# 读取pickle文件
def load_file_file():
labels = PickleFileOperator(file_path='labels.pk').read()
chars = PickleFileOperator(file_path='chars.pk').read()
label_dict = dict(zip(labels, range(len(labels))))
char_dict = dict(zip(chars, range(len(chars))))
return label_dict, char_dict
l_d, c_d = load_file_file()
print(l_d)
print(list(c_d.items())[:5])
{'体育': 0, '健康': 1, '军事': 2, '教育': 3, '汽车': 4}
[('蛎', 0), ('娼', 1), ('Ⅱ', 2), ('识', 3), ('钓', 4)]
2.2. 读取数据集样本与标签
- 读取csv文件,一行一个样本。
# load csv file
def load_csv_file(file_path):
df = pd.read_csv(file_path)
samples, y_true = [], []
for index, row in df.iterrows():
y_true.append(row['label'])
samples.append(row['content'])
return samples, y_true
s, l = load_csv_file(TRAIN_FILE_PATH)
print(s[:2])
print(l[:2])
['中国“铁腰”与英超球队埃弗顿分道扬镳,闪电般转投谢联(本赛季成功升入英超),此事运作速度之快令人惊诧。针对李铁与埃弗顿“分手”的原因、与埃弗顿主帅莫耶斯矛盾以及铁子为何选择谢联,记者昨日采访了李铁的母亲王桂芹,李母道出了李铁与埃弗顿分开的真实原因。龙菲坚决让铁子走人记者在采访王桂芹时了解到,李铁离开埃弗顿主要是妻子龙菲建议。龙菲平时不太过问李铁的足球方面事宜,但是,因为李铁长时间不能在埃弗顿踢上球,龙菲也十分焦急。多次安慰李铁后,龙菲想这样下去也不是个办法,于是索性做出决定,让李铁离开埃弗顿,只要能踢上球,去哪支球队都行。但前提条件必须是英国的球队。王妈妈告诉记者:“媳妇龙菲一直在英国学习,这孩子特别懂事,一边学习,一边还要照顾女儿和李铁的日常生活。对于李铁与埃弗顿的前前后后,龙菲一直都了解内情,因此龙菲最后告诉李铁,就是埃弗顿再请我们,我们也不去了,只要能离开埃弗顿,去哪支球队踢球都行。”据悉,龙菲2001年便在沈阳拿到了留学英国全额奖学金的录取通知书,而后,龙菲便一直在英国求学。红牌让李铁失去位置“拼命三郎”、“跑不死”、“体能王”,这些溢美之词都是称赞李铁的,不过,正是因为李铁防守时的动作过于凶狠,在英超的赛场上屡次领到红牌。过多的红牌让主帅莫耶斯逐渐对李铁失去了兴趣。对此,王妈妈向记者表示:“关于球队的相关事宜,我不太清楚。因为埃弗顿主教练莫耶斯一直都很器重李铁,因为莫耶斯的战术比较偏重防守。而在防守过程中,李铁也确实吃到过红牌,但是,我觉得教练组和俱乐部不能因为红牌的原因不让李铁上场吧。”许宏涛挽救李铁李铁成功转会埃弗顿的整个过程,国内足球著名的经纪人许宏涛功不可没。而李铁在埃弗顿后期四处碰壁的危难时刻,正是许宏涛的左右逢源,令李铁还能坚持在埃弗顿预备队踢球。后来,在埃弗顿摒弃李铁后,也正是许宏涛的人脉关系让李铁再次找到了位置。根据李铁与埃弗顿签订的合同,今年6月30日,工作合同才到期。但是,考虑到李铁与俱乐部的关系日益紧张,许宏涛便提早联系了英超其他球队,重点便是当时的英甲球队谢联。由于许宏涛是谢联董事会的成员,多次与俱乐部沟通李铁的事宜后,俱乐部最终同意了李铁加盟谢联。对李铁加盟谢联的事情,王桂芹不愿多谈。她只是表示:“许宏涛一直在帮助李铁,特别是在英国,许宏涛非常熟悉那里的环境,李铁也非常信任他,这下也好,李铁可以同郝海东一块踢球了。”首席记者贾琼', '拉齐奥获不利排序意甲本周末拉齐奥与帕尔马之战为收官阶段表现较为突出的两支球队之间的较量,两队在最近10场比赛中均取得了其中6战的胜利,主队因此提前锁定了联盟杯的参赛资格,客队更是借此早早就摆脱了赛季中段的降组威胁。目前本场比赛的赔率为主队优势的正向赔率,主胜赔率在全部42个赔率中排在了第4位,而此前的5轮竞猜中,该点位全部正路开出,已经达到了本赛季的最大值,本周末该结果极易走冷,投注时建议一搏冷门赛果。沙尔克得大庄信任近来状态不佳的沙尔克04本周末将在主场对阵斯图加特,目前本场比赛的主胜赔率处于1.70的赔率区间内,此类赔率在过去5个赛季的德甲联赛中出现次数较多,其战果统计也相对较为正路,属于纯实力对比赔率。不过在对本场比赛的赔率进行比较后可发现,奥地利著名博彩公司必赢为此战开出的1.65-3.35-5.10赔率组合对主队十分有利,主胜位明显低于目前的平均赔率,可见该公司极为看好沙尔克周末取胜,投注时可一搏主胜。蓝黑军团慎防大冷在联赛后半程排名已经敲定的情况下,国际米兰果然如人们所预料那样又一次出现了内耗问题,内部的争斗也使得曼齐尼的球队在近几轮比赛中连尝苦果,而且他们在周中又要接受意大利杯的考验,这对他们周末客场对阵卡利亚里的比赛必然要造成一定的影响。此外,我们在上期曾重点提示过,足彩竞猜第1场位的赛果已连续5轮正路开出,而且也13轮没有出现过赔率末选,两项均达到了本赛季的极限值,周末卡利亚里很有可能爆大冷击败蓝黑军团。本人心水(256元)300311033010103311330']
['体育', '体育']
2.3. 样本特征处理:句子向量化
- 使用上面标签库与字库的编号字段,把每个样本的句子数值化。
(1)文本向量化处理的参数
PAD = '<PAD>'
PAD_NO = 0
UNK = '<UNK>'
UNK_NO = 1
START_NO = UNK_NO + 1
SENT_LENGTH = 200
(2) 文本向量化
- 对句子进行长度对齐,并根据编号字典,对句子数值化。
# 文本预处理
def text_feature(labels, contents, label_dict, char_dict):
samples, y_true = [], []
for s_label, s_content in zip(labels, contents):
y_true.append(label_dict[s_label])
train_sample = []
for char in s_content:
if char in char_dict:
train_sample.append(START_NO + char_dict[char])
else:
train_sample.append(UNK_NO)
# 补充或截断
if len(train_sample) < SENT_LENGTH:
samples.append(train_sample + ([PAD_NO] * (SENT_LENGTH - len(train_sample))))
else:
samples.append(train_sample[:SENT_LENGTH])
return samples, y_true
digit_s, digit_l = text_feature(l, s, l_d, c_d)
print("对齐后,并数值化的句子:\n", digit_s[:2])
print("数值化后的标签:\n", digit_l[:2])
对齐后,并数值化的句子:
[[3436, 3896, 5108, 4825, 2824, 2987, 3767, 1156, 121, 226, 1544, 4917, 1991, 2640, 267, 2148, 4616, 25, 605, 36, 2592, 1219, 1025, 975, 4833, 3894, 4556, 3815, 2014, 713, 5133, 2235, 172, 2819, 1156, 121, 1746, 605, 2411, 2527, 2929, 757, 1733, 3054, 4573, 2902, 3486, 4517, 4907, 1868, 417, 963, 4219, 1116, 4825, 3767, 4917, 1991, 2640, 5108, 267, 2916, 2987, 3050, 3417, 3620, 4894, 3767, 4917, 1991, 2640, 2388, 2364, 1216, 93, 1603, 4283, 2074, 2368, 1885, 4825, 308, 5105, 969, 5070, 779, 4833, 3894, 605, 1037, 408, 2517, 4988, 1988, 489, 1142, 1116, 4825, 3050, 5101, 3624, 1541, 1173, 5212, 605, 1116, 5101, 2148, 4208, 1142, 1116, 4825, 3767, 4917, 1991, 2640, 267, 3626, 3050, 2236, 4003, 3417, 3620, 417, 4191, 3597, 4010, 4532, 4419, 4825, 308, 4398, 4517, 1037, 408, 625, 1988, 489, 1541, 1173, 5212, 3105, 1142, 227, 3748, 605, 1116, 4825, 2574, 3626, 4917, 1991, 2640, 2388, 1118, 1066, 1702, 308, 4191, 3597, 4553, 4950, 417, 4191, 3597, 159, 3105, 1386, 1605, 1106, 5085, 1116, 4825, 3050, 2415, 226, 2498, 149, 2527, 4718, 605, 4852, 1066, 605, 3620, 5105, 1116, 4825, 2522, 3105, 4471, 1386, 2356, 625, 4917, 1991, 2640, 4362, 4266, 226], [880, 4522, 3895, 4507, 1386, 2155, 2675, 2259, 2875, 4366, 3815, 568, 2924, 880, 4522, 3895, 3767, 4426, 3704, 491, 4573, 1513, 5105, 3076, 548, 4569, 2430, 4964, 186, 3318, 5105, 542, 4208, 3050, 2180, 5128, 226, 1544, 4573, 4471, 3050, 3318, 563, 605, 2180, 1544, 625, 125, 1826, 3401, 758, 23, 216, 2014, 3436, 361, 4445, 3985, 1142, 3407, 3436, 3121, 1513, 3050, 1133, 2155, 605, 2388, 1544, 3620, 2411, 187, 4511, 2903, 2269, 1142, 3894, 2379, 3180, 3050, 3760, 2014, 1726, 2648, 605, 4584, 1544, 3667, 1066, 4712, 2411, 3576, 3576, 3963, 1479, 1496, 1142, 2014, 713, 3436, 2430, 3050, 731, 4837, 2606, 3613, 417, 41, 4511, 3815, 23, 216, 2014, 3050, 1922, 1317, 5105, 2388, 1544, 2158, 4233, 3050, 1026, 3863, 1922, 1317, 605, 2388, 1133, 1922, 1317, 625, 1866, 4056, 4140, 194, 2725, 1922, 1317, 3436, 2675, 625, 1142, 20, 4140, 196, 605, 3657, 2411, 4511, 3050, 1788, 4187, 3850, 993, 3436, 605, 3084, 1228, 196, 1866, 4056, 1026, 5071, 3626, 4208, 605, 200, 2424, 1209, 3748, 1142, 3815, 2014, 713, 3050, 125, 3107, 1931, 605, 3815, 568, 2924, 3084, 3580, 3047, 327, 5162, 4398, 2087, 605, 975, 4531, 3105, 4553, 4950, 4804, 3096, 2087, 4535]]
数值化后的标签:
[0, 0]
2.4 生成PyTorch的数据集格式
- 因为我们使用PyTorch,所以采用PyTorch的Dataset实现数据集。方便后面训练,验证,测试使用。
# Dataset类实现
class CSVDataset(Dataset):
# load the dataset
def __init__(self, file_path):
label_dict, char_dict = load_file_file() # 读取标签库与字库
samples, y_true = load_csv_file(file_path) # 加载数据集样本
x, y = text_feature(y_true, samples, label_dict, char_dict)
# 转换为张量
self.X = T.from_numpy(np.array(x)).long()
self.y = T.from_numpy(np.array(y))
# 数据集样本数
def __len__(self):
return len(self.X)
# 返回指定索引的数据样本与标签,这是下标运算符。
def __getitem__(self, idx):
return [self.X[idx], self.y[idx]]
# 根据比例把数据集分成训练集与测试集。
def get_splits(self, n_test=0.3):
# determine sizes
test_size = round(n_test * len(self.X))
train_size = len(self.X) - test_size
# calculate the split
return random_split(self, [train_size, test_size])
ds = CSVDataset(TRAIN_FILE_PATH)
print(ds[0])
[tensor([3436, 3896, 5108, 4825, 2824, 2987, 3767, 1156, 121, 226, 1544, 4917,
1991, 2640, 267, 2148, 4616, 25, 605, 36, 2592, 1219, 1025, 975,
4833, 3894, 4556, 3815, 2014, 713, 5133, 2235, 172, 2819, 1156, 121,
1746, 605, 2411, 2527, 2929, 757, 1733, 3054, 4573, 2902, 3486, 4517,
4907, 1868, 417, 963, 4219, 1116, 4825, 3767, 4917, 1991, 2640, 5108,
267, 2916, 2987, 3050, 3417, 3620, 4894, 3767, 4917, 1991, 2640, 2388,
2364, 1216, 93, 1603, 4283, 2074, 2368, 1885, 4825, 308, 5105, 969,
5070, 779, 4833, 3894, 605, 1037, 408, 2517, 4988, 1988, 489, 1142,
1116, 4825, 3050, 5101, 3624, 1541, 1173, 5212, 605, 1116, 5101, 2148,
4208, 1142, 1116, 4825, 3767, 4917, 1991, 2640, 267, 3626, 3050, 2236,
4003, 3417, 3620, 417, 4191, 3597, 4010, 4532, 4419, 4825, 308, 4398,
4517, 1037, 408, 625, 1988, 489, 1541, 1173, 5212, 3105, 1142, 227,
3748, 605, 1116, 4825, 2574, 3626, 4917, 1991, 2640, 2388, 1118, 1066,
1702, 308, 4191, 3597, 4553, 4950, 417, 4191, 3597, 159, 3105, 1386,
1605, 1106, 5085, 1116, 4825, 3050, 2415, 226, 2498, 149, 2527, 4718,
605, 4852, 1066, 605, 3620, 5105, 1116, 4825, 2522, 3105, 4471, 1386,
2356, 625, 4917, 1991, 2640, 4362, 4266, 226]), tensor(0, dtype=torch.int32)]
3. 词嵌入处理
- 一般会直接使用词嵌入,但是这里对汉字使用预训练的词嵌入方式,对句子进行特征向量化处理,可以确保模型训练效果更好。
- 这里的预训练模型,采用维基百科中语料库训练的词向量模型:
sgns.wiki.char.bz2
- 每个字都需要转换为向量。
- 这里的预训练模型,采用维基百科中语料库训练的词向量模型:
- 下面使用上面字库中的字,查询已经预训练的词向量模型中训练的向量,得到满足我们这里使用的词向量
import torch
from gensim.models import KeyedVectors
# 读取标签库与字库
label_dict, char_dict = load_file_file()
# 加载预训练的词向量模型
em_model = KeyedVectors.load_word2vec_format('./datasets/sgns.wiki.char.bz2',
binary=False,
encoding="utf-8",
unicode_errors="ignore")
# 使用gensim载入word2vec词向量
"""
4是考虑未来加入四个特殊字符:<PAD>,<UNK>,<START>,<END>
300是预训练的时候就设置为300:具体可以参考:https://github.com/Embedding/Chinese-Word-Vectors?tab=readme-ov-file
实际这里数据集统计的字库没有5500,下面pretrained_vector的后面行都是0。
"""
pretrained_vector = torch.zeros(NUM_WORDS + 4, 300).float() # 存放字库中每个字的词向量
# print(model.index2word)
for char, index in char_dict.items():
if char in em_model.key_to_index:
# 把字转换为向量
vector = em_model.get_vector(char)
# print(vector)
pretrained_vector[index, :] = torch.from_numpy(vector.copy()) # 使用copy是因为get_vector返回的numpy数组是不可写的。不加会有警告
print(vector.flags['WRITEABLE'])
print(vector.copy().flags['WRITEABLE'])
False
True
pretrained_vector[-1]
tensor([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
pretrained_vector[0]
tensor([-2.5166e-01, 2.1260e-03, -7.5505e-01, -5.7399e-02, -4.0988e-02,
-2.5291e-01, 5.3310e-02, -1.3894e-02, 4.3891e-01, -3.8147e-02,
-2.0128e-01, -5.9504e-02, 4.8097e-02, 1.0573e-01, -6.1304e-02,
-2.1859e-01, 4.8095e-01, -2.3189e-01, -4.5559e-01, 3.5048e-01,
2.8622e-01, 1.5197e-01, -4.5313e-02, -5.5626e-02, -8.5551e-02,
-6.2766e-02, 9.7919e-02, 3.8548e-01, 1.8273e-01, 5.4902e-02,
-4.4603e-02, -2.7428e-01, 1.7315e-02, 4.8173e-02, -1.0188e-02,
-1.1564e-01, 1.3562e-01, -8.5669e-02, -1.2031e-01, 3.4792e-01,
1.6377e-01, 1.7365e-01, 4.0493e-01, -2.6211e-01, -2.8300e-01,
-2.4447e-02, -1.7962e-01, -9.1980e-03, 2.4517e-01, 1.4564e-01,
1.8893e-01, 6.1344e-01, 1.4634e-01, -3.7221e-01, 1.3984e-01,
-1.6315e-01, 1.7710e-02, -2.2459e-01, 3.1234e-02, -2.7014e-01,
-2.1277e-01, -9.9185e-02, 1.1965e-01, -3.5157e-02, -3.1400e-04,
1.0341e-01, -4.5918e-01, -1.3590e-01, -1.9133e-01, -2.5318e-01,
1.6349e-01, -3.9125e-01, -7.9844e-02, -3.9014e-02, 4.2832e-01,
5.4695e-02, -3.2569e-01, -1.0863e-01, 1.0948e-01, 5.0902e-01,
6.1270e-01, -1.9650e-01, -4.1784e-02, 9.8486e-02, -2.8914e-01,
1.1830e-01, 2.1662e-01, -2.6285e-01, 2.2754e-01, -3.3230e-01,
-2.9382e-01, -2.1537e-01, -5.5550e-01, -3.0106e-02, 6.3398e-02,
8.9900e-03, 2.1025e-01, 1.2269e-01, -2.0311e-01, -3.8709e-01,
-3.1572e-01, 3.0690e-01, -1.9227e-01, -3.4366e-01, 7.7620e-02,
4.7994e-01, 2.4226e-01, -1.0725e-01, -1.0820e-01, 1.4680e-01,
-1.6433e-01, 1.7356e-01, 1.6682e-02, 2.3170e-01, -1.2936e-01,
1.8013e-01, -1.2464e-01, -2.2828e-01, -2.3223e-01, -5.9250e-03,
2.3588e-01, -3.7569e-01, 5.0721e-01, 7.0246e-01, -2.6877e-01,
2.8580e-03, -5.8815e-01, -2.3668e-01, 1.0971e-01, -8.2170e-03,
1.2551e-01, 1.8670e-02, 4.5151e-01, 6.8175e-02, 2.0498e-01,
4.5140e-01, 5.1324e-01, -4.8228e-02, -1.7520e-03, 6.9598e-02,
-4.7379e-02, -2.3501e-01, -4.1574e-01, 1.1202e-01, -4.1136e-01,
-2.2400e-01, -1.1157e-01, 3.9643e-01, 1.7197e-01, -7.1166e-02,
2.2666e-01, 4.9972e-01, -5.9917e-01, -5.2575e-01, -3.8444e-01,
2.9197e-01, -2.6319e-01, -2.6827e-01, -1.7151e-01, -3.2219e-01,
-1.5482e-01, 4.4596e-01, 1.1041e-01, 3.2358e-01, 1.1809e-01,
7.4830e-03, 3.9770e-01, 2.3340e-01, 6.3971e-01, -7.0496e-01,
-1.2747e-01, 1.5125e-01, 2.0257e-01, 2.9059e-01, 5.4421e-02,
-5.9573e-01, 1.8627e-02, -2.0663e-01, 2.4536e-01, -3.1686e-01,
1.5185e-01, 3.5283e-02, -2.4756e-01, 2.7790e-01, -1.1016e-01,
-1.4018e-01, 2.4151e-01, -7.5792e-02, -4.4470e-01, -3.0382e-01,
8.3656e-02, -1.0520e-01, -6.6970e-03, 2.0030e-01, -2.7011e-01,
1.0509e-01, 2.1204e-01, 1.9944e-01, -2.2444e-01, -1.9029e-01,
-3.3236e-01, -7.9911e-02, -3.7321e-01, 9.8192e-02, -1.9179e-01,
2.6793e-01, 4.5805e-01, -2.5262e-01, -1.1888e-01, -2.9169e-01,
2.9650e-01, 4.0774e-01, -1.3908e-01, 1.6033e-01, -4.0140e-02,
-3.6502e-01, 2.9890e-01, 6.8221e-01, -4.8779e-01, 2.5828e-01,
-2.7593e-01, -1.2254e-01, -3.9470e-02, -2.1260e-01, -2.3199e-02,
-2.7077e-01, 3.8680e-02, -1.8343e-01, -2.1692e-02, -2.4166e-01,
1.1560e-01, 8.0079e-02, 2.1750e-03, -1.9942e-02, -2.9017e-01,
-2.7840e-01, 2.2855e-01, -3.2480e-01, -2.2139e-01, 1.9187e-01,
-4.6475e-01, 5.2336e-01, 5.4522e-01, -1.0142e-01, -3.1336e-01,
1.4690e-01, 8.9748e-02, 2.2159e-01, -7.5918e-01, -2.7461e-01,
-8.2008e-02, -3.2914e-01, -2.8129e-01, 3.4548e-01, 2.3467e-01,
-5.6391e-02, 1.4375e-02, 3.8655e-01, -2.0344e-01, -2.4192e-01,
5.5580e-01, -3.3075e-01, 2.6455e-01, 3.5124e-01, 2.4330e-01,
1.5741e-01, 1.0453e-02, 8.6976e-02, 3.3163e-01, 2.9760e-01,
5.1001e-02, 7.9290e-02, 4.2176e-01, 3.4901e-02, 6.3282e-01,
-3.1701e-01, 3.7667e-01, -1.0663e-01, -2.6375e-01, -5.9062e-01,
2.2802e-01, 1.2913e-01, 5.9333e-01, -1.1817e-02, -1.9145e-02,
9.6389e-02, 2.0213e-01, 2.5641e-01, 5.0276e-01, 3.5181e-02,
-3.3445e-01, -5.2460e-03, -1.3024e-01, 3.0163e-01, -2.7992e-01,
-2.3243e-01, -1.5426e-01, -4.0426e-01, -1.8360e-02, 8.2140e-03])
- 下面简单演示下,词嵌入向量的使用
import math
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
# 词嵌入对象
emb = nn.Embedding.from_pretrained(pretrained_vector, freeze=False, padding_idx=0)
# 对数据集进行批次处理
train_dl = DataLoader(ds, batch_size=5, shuffle=True) # 每个批次5个样本
for x_batch, y_batch in train_dl:
v_emb = emb(x_batch)
print(v_emb.shape)
break
torch.Size([5, 200, 300])
-
词向量化后,一个句子中的每个词都转化为一个向量。
- 5是一个批次中的样本数:5个句子
- 200是句子长度
- 300是预训练的词向量的特征维度。就是每个词使用300长的向量表示其特征。
因为采用批次的方式,所以每个句子需要补齐。这样才能满足矩阵运算中对形状的要求。
4. Transformer模型实现
- 这里的模型没有使用PyTroch进行原生实现,而是利用PyTorch的封装实现:
- TransformerEncoderLayer:编码器单元
- TransformerEncoder:编码器
- 因为位置编码在PyTorch中没有实现,需要自己实现。
4.1. 位置编码
-
位置编码的计算公式如下:
- 偶位置:
- 奇位置:
-
参数解释:
- 表示单子在句子中位置
- 表示位置编码的维度,这个维度必须与词嵌入的维度一直。在上面采用的额是预训练的维度:300。
-
表示偶数维度,表示奇数维度。
- ,,
-
下面的实现来自Pytorch官方文档:
https://pytorch.org/tutorials/beginner/transformer_tutorial.html
- 关于位置编码实际有个发展过程,Pytorch官方文档的实现与上面原始论文中提出的计算公式不一样,有微小的变化。
- 这里不纠结位置编码的具体计算公式,后面会单独说明。
class PositionalEncoding(nn.Module):
def __init__(self, d_model, vocab_size=5000, dropout=0.1):
super().__init__()
self.dropout = nn.Dropout(p=dropout)
pe = torch.zeros(vocab_size, d_model)
# torch.arange(0, vocab_size, dtype=torch.float):生成0-vocab_size的张量,shape=(vocab_size,)
# unsqueeze(1):增加1维,变成2维张量。2维张量的shape=(vocab_size, 1)
position = torch.arange(0, vocab_size, dtype=torch.float).unsqueeze(1)
# exp:自然指数运算
div_term = torch.exp(
torch.arange(0, d_model, 2).float()
* (-math.log(10000.0) / d_model)
)
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0) # 增加1维,vocab_size所在维变成第二维
self.register_buffer("pe", pe)
def forward(self, x):
"""
X的第一维表示批次数,每行是一个样本。
位置编码对每个句子处理一样。所以X的第一维是n=5,PE第一维是1
"""
# print("x的形状", x.shape)
# print("PE的形状", self.pe.shape)
x = x + self.pe[:, : x.size(1), :] # : x.size(1)限制与x的句子长度一致。
# print("截断后的维数:", self.pe[:, : x.size(1), :].shape)
return self.dropout(x)
4.2. Transformer分类器
- Pytorch已经实现编码器:
- 编码单元:TransformerEncoderLayer
- 编码器:编码器
- 分类器使用Pytorch的逻辑回归:
- 全连接层,加一个sigmoid运算,实际这里使用的是softmax函数。
EMBEDDING_SIZE = 300
class TextClassifier(nn.Module):
def __init__(
self,
nhead=8, # 多头自注意力的多头个数
dim_feedforward=2048, # 前馈网络的大小
num_layers=6, # 编码器中编码单元的个数
dropout=0.1,
activation="relu", # 激活函数
classifier_dropout=0.1):
super().__init__()
vocab_size = NUM_WORDS + 2 # 这个大小不影响运算,实际不同的语料库,计算的vocab_size也不一样。
d_model = EMBEDDING_SIZE
# vocab_size, d_model = embeddings.size()
assert d_model % nhead == 0, "nheads 必须整除 d_model"
# Embedding layer definition
# self.emb = nn.Embedding(vocab_size, d_model, padding_idx=0)
# 词嵌入对象:使用预训练模型
self.emb = nn.Embedding.from_pretrained(pretrained_vector, freeze=False, padding_idx=0)
# 位置编码器
self.pos_encoder = PositionalEncoding(
d_model=d_model,
dropout=dropout,
vocab_size=vocab_size
)
# 编码单元
encoder_layer = nn.TransformerEncoderLayer(
d_model=d_model,
nhead=nhead,
dim_feedforward=dim_feedforward,
dropout=dropout,
batch_first=True # 提高性能,否则会出现警告
)
# 编码器
self.transformer_encoder = nn.TransformerEncoder(
encoder_layer,
num_layers=num_layers
)
# 分类器:5是最后分类的类别数,这里采用一层分类
self.classifier = nn.Linear(d_model, 5)
self.d_model = d_model
def forward(self, x):
# 词嵌入运算
x = self.emb(x) * math.sqrt(self.d_model) # 对词嵌入向量做了额外的scaled计算,方式梯度消失
# 位置编码运算
x = self.pos_encoder(x)
# 编码器处理
x = self.transformer_encoder(x)
# 使用均值降维
x = x.mean(dim=1)
# 分类计算
x = self.classifier(x)
# 这里没有直接转换为概率softmax运算,这个对训练没有影响,主要在分类方便。
return x
- 下面是测试模型的运算:
- 没有训练过的模型,只是分类效果差而已,实际已经可以使用了。
model = TextClassifier(
nhead=10, # 多头数量,记得与d_model有整除关系
dim_feedforward=128, # 前馈全连接神经网络的维度
num_layers=1, # 编码器层数
dropout=0.0,
classifier_dropout=0.0)
# 上面的批次是5,分类标签个数是5,输出结果是没有经过概率化的,就是sigmoid或者softmax运算
for x_batch, y_batch in train_dl:
y_ = model(x_batch)
print(y_.shape)
print(y_) # 概率化后,概率最大的下标就是分类的标签编号。
break
torch.Size([5, 5])
tensor([[-0.2383, 0.3347, 0.2001, -0.1349, 0.1200],
[-0.1407, 0.3566, 0.3740, -0.2075, 0.0439],
[-0.1911, 0.2526, 0.2672, -0.3290, -0.0506],
[-0.2626, 0.1256, 0.3350, -0.3126, 0.2457],
[-0.1740, 0.2923, 0.2634, -0.2818, -0.0333]],
grad_fn=<AddmmBackward0>)
5. Transformer训练实现
- Transfoemer的实现与一般深度学习神经网络的实现一样:
- 对训练样本进行迭代开始训练
- 调用模型,计算模型输出
- 使用模型输出与已知标签计算误差
- 对误差求导,得到更新值
- 反向更新所有模型参数
- 可选:使用模型预测,并统计分类准确率(评估)
- 继续下一次训练。
import torch
from torch.optim import Adam # 优化器
from torch.nn import CrossEntropyLoss, Softmax # 损失函数,与概率转化函数
from torch.utils.data import DataLoader # 批次数据集
from numpy import vstack, argmax # argmax是预测的常用函数,得到概率最大下标(就是预测分类结果)
from sklearn.metrics import accuracy_score # 度量精确度
TRAIN_BATCH_SIZE = 32 # 批次大小,我们前面使用的是5
TEST_BATCH_SIZE = 16 # 测试批次大小,可以设置为1,就是一个一个样本测试
LEARNING_RATE = 0.001 # 学习率
EPOCHS = 10 # 训练轮次
5.1. 训练实现
- 深度学习的训练模式基本上固化了
class ModelTrainer(object):
# 评估
@staticmethod
def evaluate_model(test_dl, model):
# 预测
predictions, actuals = [], []
# 迭代预测
for i, (inputs, targets) in enumerate(test_dl):
# 预测结果
yhat = model(inputs)
# 转换为numpy数组
yhat = yhat.detach().numpy()
# 样本标签(真实标签)
actual = targets.numpy()
# 转换为分类标签编号(不需要使用softxmax,因为这是递增函数)
yhat = argmax(yhat, axis=1) # 预测标签
# 对预测结果进行形状处理,并放入一个列表,并利用numpy的vstack合并成一个预测结果
actual = actual.reshape((len(actual), 1))
yhat = yhat.reshape((len(yhat), 1))
# store
predictions.append(yhat)
actuals.append(actual)
predictions, actuals = vstack(predictions), vstack(actuals)
# 计算精确度
acc = accuracy_score(actuals, predictions)
return acc
# 训练,评估,训练参数
def train(self, model):
# 加载训练数据集与测试数据集
train, test = CSVDataset(TRAIN_FILE_PATH), CSVDataset(TEST_FILE_PATH)
# 转换为批次数据集
train_dl = DataLoader(train, batch_size=TRAIN_BATCH_SIZE, shuffle=True)
test_dl = DataLoader(test, batch_size=TEST_BATCH_SIZE)
# 定义优化器
optimizer = Adam(model.parameters(), lr=LEARNING_RATE)
# 开始轮次训练
for epoch in range(EPOCHS):
# 对训练样本进行批次训练。
for x_batch, y_batch in train_dl:
y_batch = y_batch.long()
# 梯度置零
optimizer.zero_grad()
# 计算预测值
y_pred = model(x_batch)
# 使用预测值与真实标签进行计算误差
loss = CrossEntropyLoss()(y_pred, y_batch)
# 对误差进行求导,得到梯度。
loss.backward()
# 更新梯度
optimizer.step()
# 评估
test_accuracy = self.evaluate_model(test_dl, model)
print("轮次: %d, 损失值: %.5f, 测试集准确率: %.5f" % (epoch+1, loss.item(), test_accuracy))
5.2. 训练执行
model = TextClassifier(
nhead=10, # 多头自注意力数量
dim_feedforward=128, # 解码器单元的前馈全连接网络维度
num_layers=4, # 编码器的层数
dropout=0.0,
classifier_dropout=0.0)
# 统计参数量
num_params = sum(param.numel() for param in model.parameters())
print("参数量:", num_params)
# 训练
ModelTrainer().train(model)
# 保存训练模型
torch.save(model, 'model.pth')
参数量: 3411217
轮次: 1, 损失值: 0.52407, 测试集准确率: 0.87879
轮次: 2, 损失值: 0.63633, 测试集准确率: 0.88485
轮次: 3, 损失值: 0.30823, 测试集准确率: 0.89091
轮次: 4, 损失值: 0.22140, 测试集准确率: 0.87273
轮次: 5, 损失值: 0.18218, 测试集准确率: 0.87071
轮次: 6, 损失值: 0.23782, 测试集准确率: 0.88687
轮次: 7, 损失值: 0.28823, 测试集准确率: 0.90707
轮次: 8, 损失值: 0.20676, 测试集准确率: 0.88081
轮次: 9, 损失值: 0.14725, 测试集准确率: 0.85859
轮次: 10, 损失值: 0.20790, 测试集准确率: 0.88889
6. 模型评估
- 利用sklearn工具,对测试集预测结果计算分类报告与混淆矩阵。
import torch as T
from torch.utils.data import DataLoader
import numpy as np
import pandas as pd
from sklearn.metrics import classification_report, confusion_matrix
import torch.nn.functional as F
import matplotlib.pyplot as plt
from matplotlib import rcParams # 显示汉字
6.1. 计算预测结果
- 预测结果需要进行如下处理
- 加载测试数据集,得到样本与真实标签
- 利用样本计算预测结果
(1) 计算预测结果
# 加载模型
model = T.load('model.pth')
# 加载测试数据集
test_ds = CSVDataset(TEST_FILE_PATH)
test_dl = DataLoader(test_ds, batch_size=len(test_ds)) # 做成一个批次
# 循环预测
for x, y in test_dl:
y_ = model(x) # 预测
y_ = y_.detach().numpy()
y_ = argmax(y_, axis=1)
y = y.detach().numpy()
print(y_[:5], y[:5])
[0 1 0 1 0] [0 0 0 0 0]
(2)把预测标签编号转换为文字
# 记载标签库与字库
label_dict, _ = load_file_file()
# 把key与value交换
label_dict_rev = {v: k for k, v in label_dict.items()}
true_labels = []
pred_labels = []
for true_no, pred_no in zip(y, y_):
true_label = label_dict_rev[true_no]
pred_label = label_dict_rev[pred_no]
true_labels.append(true_label)
pred_labels.append(pred_label)
# 打印5个看看效果
print(true_labels[:5], pred_labels[:5])
['体育', '体育', '体育', '体育', '体育'] ['体育', '健康', '体育', '健康', '体育']
6.2. 计算分类报告
- 调用
classification_report
输出分类报告
report = classification_report(true_labels, pred_labels, digits=5) # digits指定输出的有效小数位数
print(report)
precision recall f1-score support
体育 0.94505 0.86869 0.90526 99
健康 0.79464 0.89899 0.84360 99
军事 0.94737 0.90909 0.92784 99
教育 0.83654 0.87879 0.85714 99
汽车 0.94624 0.88889 0.91667 99
accuracy 0.88889 495
macro avg 0.89397 0.88889 0.89010 495
weighted avg 0.89397 0.88889 0.89010 495
6.3. 计算混淆矩阵
- 调用
confusion_matrix
输出混淆矩阵-
confusion_matrix
输出的矩阵可以使用matplotlib可视化。
-
label_names = list(label_dict.keys())
C_M = confusion_matrix(true_labels, pred_labels, labels=label_names) # 最后是标签名,需要类型是list
print(C_M)
[[86 7 2 4 0]
[ 0 89 1 6 3]
[ 1 2 90 6 0]
[ 1 9 0 87 2]
[ 3 5 2 1 88]]
- 使用matplotlib可视化
rcParams['font.family'] = 'SimHei'
plt.matshow(C_M, cmap=plt.cm.Reds) # cmap指定颜色系
# 显示刻度与标签
ticks = np.array(range(len(label_names)))
plt.xticks(ticks, label_names, rotation=90) # 将标签印在x轴坐标上, 旋转90度
plt.yticks(ticks, label_names) # 将标签印在y轴坐标上
plt.show()
# plt.savefig("./image/confusion_matrix.png") # 直接保存为图片
7. 推理
- 推理实现也是常见的流程:
- 加载模型
- 预处理需要分类的文本
- 预测计算
- 处理预测结果
import torch as T
import numpy as np
import torch.nn.functional as F
# 记载模型
model = T.load('model.pth')
# 加载标签库与字库
label_dict, char_dict = load_file_file()
# 交换key与value
label_dict_rev = {v: k for k, v in label_dict.items()}
# 分类预测文本
text = '盖世汽车讯,特斯拉去年击败了宝马,夺得了美国豪华汽车市场的桂冠,并在今年实现了开门红。1月份,得益于大幅降价和7500美元美国电动汽车税收抵免,特斯拉再度击败宝马,蝉联了美国豪华车销冠,并且注册量超过了排名第三的梅赛德斯-奔驰和排名第四的雷克萨斯的总和。根据Experian的数据,在所有豪华品牌中,1月份,特斯拉在美国的豪华车注册量为49,917辆,同比增长34%;宝马的注册量为31,070辆,同比增长2.5%;奔驰的注册量为23,345辆,同比增长7.3%;雷克萨斯的注册量为23,082辆,同比下降6.6%。奥迪以19,113辆的注册量排名第五,同比增长38%。凯迪拉克注册量为13,220辆,较去年同期增长36%,排名第六。排名第七的讴歌的注册量为10,833辆,同比增长32%。沃尔沃汽车排名第八,注册量为8,864辆,同比增长1.8%。路虎以7,003辆的注册量排名第九,林肯以6,964辆的注册量排名第十。'
# 文本向量化,因为text_feature实现的缘故,其中需要一个labels参数,但实际该参数在推理没有意义,所以使用随意一个标签代替。
labels, contents = ['汽车'], [text]
samples, y_true = text_feature(labels, contents, label_dict, char_dict)
# 转化为张量
x = T.from_numpy(np.array(samples)).long()
print(x.shape)
# 预测,注意x的形状按照我们前面说的,需要满足特定的形状
y_pred = model(x)
# 转换为概率
y_numpy = F.softmax(y_pred, dim=1).detach().numpy()
# 去最大概率的下标作为预测标签编号(因为可能存在多个文本预测结果)
predict_list = np.argmax(y_numpy, axis=1).tolist()
# 查询输出预测标签
for i, predict in enumerate(predict_list):
print(f"第{i+1}个文本,预测标签为: {label_dict_rev[predict]}")
torch.Size([1, 200])
第1个文本,预测标签为: 汽车