整体过程就是首先拿到了数据集微博100K,对个这个评论数据集进行处理,分类标签和评论内容。对评论内容进行分词处理,之后进行词频统计对高词频的进行编码,低词频的进用《UNK》表示,并使用《PAD》把他们扩展到等长列表便于处理。
拿到处理好的数据集,将其输入通过嵌入层得到密集表示向量表示,之后通过LSTM层,激活函数层,池化层,全连接层,最后应用softmax激活函数,得到类别概率。
1 数据准备
训练集 微博评论100K
停用词 采用合工大停用词表
对数据进行处理->拿到字典->利用字典对语料信息进行编码
1 读取评论和停用词文件
2 对评论内容进行分词处理并进行停用词过滤操作
3 根据分词结果统计词典
4 对词典进行排序 取top n的词作为最终词典,其余词作为同一个字符(因为不重要,提高运算速度)(UNK,PAD放最后)
data_processing.py
# coding=utf-8import jieba # jieba中文分词使用# 微博评论数据路径
data_path = "sources/weibo_senti_100k.csv"
# 停用词路径
data_stop_path = "sources/hit_stopword.txt"
# 读取文件中的每一行,并将每一行作为一个字符串存储在一个列表中,跳过第一行
data_list = open(data_path, encoding="utf-8").readlines()[1:]
# 拿到停用词列表
stops_word = open(data_stop_path, encoding="utf-8").readlines()
# 过滤停用词换行符
stops_word = [line.strip() for line in stops_word]
# 手动添加过滤空格和换行
stops_word.append(" ")
stops_word.append("\n")# 定义统计字典
voc_dict = {}
# 字典中的最小词频数
min_seq = 1
# 取字典中前top_n个
top_n = 1000
# 对于前1000以为的值为UNK
UNK = "<UNK>"
# NLP建模需要PAD到固定长度
PAD = "<PAD>"# 对每条评论进行分词处理
for item in data_list:# 标签 1代表积极 0代表消极label = item[0]# 评论内容content = item[2:].strip() # .strip() 方法去除前后的空格。# 使用jieba进行分词 例:梦想 有 多 大 , 舞台 就 有 多 大 ! 鼓掌seg_list = jieba.cut(content, cut_all=False)# 去除停用词后的最终分词结果seg_res = []# 去除停用词并创建词频字典for seg_item in seg_list:# 判断分词词语是否为停用词if (seg_item in stops_word):continueseg_res.append(seg_item)# seg_res : ['梦想', '大', '舞台', '大', '!', '鼓掌']# 判断该词语是否在字典中if seg_item in voc_dict.keys():voc_dict[seg_item] = voc_dict[seg_item] + 1 # 在的话词频+1else:voc_dict[seg_item] = 1 # 不在就加进字典去# voc_dict:{'\ufeff': 1, '更博': 1, '爆照': 2, '帅': 3, '越来越': 1, '爱': 4, '生快': 1, '傻': 1, '缺': 1}# 对字典进行排序
voc_list = sorted([_ for _ in voc_dict.items() if _[1] > min_seq],key=lambda x: x[1], # key=lambda x:x[1]:指定排序的依据是每个项中的第二个元素(即值)。reverse=True)[:top_n]
"""
for idx, word_count in enumerate(voc_list):print(idx," ",word_count)0 ('爱', 6)
1 ('都', 2)
2 ('大', 2)
"""# 将处理后的字典重新赋值给voc_dict
voc_dict = {word_count[0]: idx for idx, word_count in enumerate(voc_list)} # enumerate得到voc_list的键值和索引值# UNK:len(voc_dict) 表示将 UNK 这个词映射到一个特定的索引值 len(voc_dict)。这意味着,如果在后续的处理中遇到 UNK,它将被视为一个有效的词汇,并被分配一个特定的索引。
# PAD:len(voc_dict)+1 类似地,将 PAD 映射到一个比 voc_dict 中最后一个词汇的索引大 1 的值。这通常用于填充序列,使其达到固定的长度。
voc_dict.update({UNK: len(voc_dict), PAD: len(voc_dict) + 1})# 保存字典
ff = open("sources/dict", "w", encoding="utf-8")
for item in voc_dict.keys():ff.writelines("{},{}\n".format(item, voc_dict[item]))
ff.close()
dataset类的定义 自定义训练模型的数据类
作用 利用字典对词进行编码操作 拿到编码后的数据集
1 实现text_CLS(Dataset):类,包括初始化,获取长度,获得编码后的分词数据集
2 实现初始化
通过load_data对数据集进行分词,过滤停用词,拿到数据集的数据字典和最大分词长度
3 实现获取长度
获得数据集长度
4 实现获得编码后的分词数据集
编码是自己之保存了词频超过1的,词频为1的用UNK表示,为了统一长度用PAD的编码填充。
datasets.py
from torch.utils.data import Dataset, DataLoader # 从 torch 库中导入数据集(Dataset)和数据加载器(DataLoader)类
import jieba # 导入中文分词库 jieba
import numpy as np # 导入科学计算库 numpy# 在 PyTorch 中,Dataset 是一个抽象类,用于表示数据集。它提供了一种统一的接口,用于读取和处理数据,
# 并与 DataLoader 结合使用,实现数据的批处理和迭代加载。# 读取字典
def read_dict(voc_dict_path):voc_dict = {}# 读取字典中的内容 {泪,0 嘻嘻,1 都,2 爱,3...}dict_list = open(voc_dict_path, encoding="utf-8").readlines()for item in dict_list:item = item.split(",")voc_dict[item[0]] = int(item[1].strip())# 拿到字典return voc_dict#加载数据
#获得 [['1', ['\ufeff', '更博', '爆照', '帅', '越来越', '爱', '生快', '傻', '缺', '爱', '爱', '爱']], ['1', ['张晓鹏', 'jonathan', '土耳其', '事要', '认真对待', '直接', '开除', '丁丁', '看', '世界', '很', '细心', '酒店', '都', '全部', 'OK']]]
#和 最大的字典项长度
def load_data(data_path, data_stop_path):# 读取文件中的每一行,并将每一行作为一个字符串存储在一个列表中,跳过第一行data_list = open(data_path, encoding="utf-8").readlines()[1:]# 拿到停用词列表stops_word = open(data_stop_path, encoding="utf-8").readlines()# 过滤停用词换行符stops_word = [line.strip() for line in stops_word]# 手动添加过滤空格和换行stops_word.append(" ")stops_word.append("\n")# 定义统计字典voc_dict = {}# 定义存储最终数据data = []# 最长句子的长度max_len_seq = 0# 对每条评论进行分词处理for item in data_list:# 标签 1代表积极 0代表消极label = item[0]# 评论内容content = item[2:].strip() # .strip() 方法去除前后的空格。# 使用jieba进行分词 例:梦想 有 多 大 , 舞台 就 有 多 大 ! 鼓掌seg_list = jieba.cut(content, cut_all=False)# 去除停用词后的最终分词结果seg_res = []# 去除停用词并创建词频字典for seg_item in seg_list:# 判断分词词语是否为停用词if (seg_item in stops_word):continueseg_res.append(seg_item)# seg_res : ['梦想', '大', '舞台', '大', '!', '鼓掌']# 判断该词语是否在字典中if seg_item in voc_dict.keys():voc_dict[seg_item] = voc_dict[seg_item] + 1 # 在的话词频+1else:voc_dict[seg_item] = 1 # 不在就加进字典去# voc_dict:{'\ufeff': 1, '更博': 1, '爆照': 2, '帅': 3, '越来越': 1, '爱': 4, '生快': 1, '傻': 1, '缺': 1}# 记录最长句子的长度 #将句子PAD到固定的长度 以最长的句子为基准if len(seg_res) > max_len_seq:max_len_seq = len(seg_res)# 添加到数据集data [['1', ['\ufeff', '更博', '爆照', '帅', '越来越', '爱', '生快', '傻', '缺', '爱', '爱', '爱']], ['1', ['张晓鹏', 'jonathan', '土耳其', '事要', '认真对待', '直接', '开除', '丁丁', '看', '世界', '很', '细心', '酒店', '都', '全部', 'OK']]]data.append([label, seg_res])# print(data)return data, max_len_seqclass text_CLS(Dataset):def __init__(self, voc_dict_path, data_path, data_stop_path):# 微博评论数据路径self.data_path = data_path# 停用词路径self.data_stop_path = data_stop_path# 拿到字典self.voc_dict = read_dict(voc_dict_path)# 加载训练数据self.data, self.max_len_path = load_data(self.data_path, self.data_stop_path)# 随机打乱一个数组。np.random.shuffle(self.data)def __len__(self):return len(self.data)def __getitem__(self, item):data = self.data[item] # ['1', ['\ufeff', '更博', '爆照', '帅', '越来越', '爱', '生快', '傻', '缺', '爱', '爱', '爱']]lable = int(data[0]) # 1word_list = data[1] # ['\ufeff', '更博', '爆照', '帅', '越来越', '爱', '生快', '傻', '缺', '爱', '爱', '爱']input_idx = []# 将word转化为对应的idfor word in word_list:if word in self.voc_dict.keys(): # 在字典中取字典的编码input_idx.append(self.voc_dict[word])else: # 不在取UNKinput_idx.append(self.voc_dict["<UNK>"])# 对齐长度 以最大的为基准if len(input_idx) < self.max_len_path:input_idx += [self.voc_dict["<PAD>"] for _ in range(self.max_len_path - len(input_idx))]# 转为数组data = np.array(input_idx)return lable, datadef data_loader(dataset,config):# # 微博评论数据路径# data_path = "sources/weibo_senti_100k.csv"# # 停用词路径# data_stop_path = "sources/hit_stopword.txt"# # 字典路径# dict_path = "sources/dict"return DataLoader(dataset, batch_size=config.batch_size, shuffle=config.is_shuffle)#将数据集拆分为小批次,并在训练或推理过程中按批次提供数据。if __name__ == '__main__':# 微博评论数据路径data_path = "sources/weibo_senti_100k.csv"# 停用词路径data_stop_path = "sources/hit_stopword.txt"# 字典路径dict_path = "sources/dict"train_dataloader= data_loader(dict_path,data_path,data_stop_path)for i,batch in enumerate(train_dataloader):print(batch)
config.py 用于配置训练模型
import torchclass Config():def __init__(self):# 词汇表的大小,表示模型可以处理的不同词汇的数量。self.n_vocab = 1002# 词嵌入的维度,即将词索引转换为密集向量表示的维度。self.embed_size = 128# LSTM隐藏状态的维度,决定了模型的记忆容量和表示能力。self.hidden_size = 128# LSTM层的数量,表示堆叠的LSTM层的层数。self.num_layers = 3# 用于控制模型中的dropout概率,以减少过拟合。self.dropout = 0.8# 模型要预测的类别数量。self.num_classes = 2# 输入序列的固定长度,用于处理变长序列的情况。self.pad_size = 32self.batch_size=128self.is_shuffle=Trueself.numepochs = 100#学习率self.learn_rate=0.001# 用于指定模型在哪个设备上运行,如果CUDA可用,则使用GPU进行加速,否则使用CPU。self.devices = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model.py
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as npclass Model(nn.Module):# config网络搭建所需要的参数def __init__(self, config):super(Model, self).__init__()# 词嵌入层 将输入序列转化为向量 嵌入层用于将输入的离散化的文本数据转换为连续的稠密向量表示。 嵌入向量通常是随机初始化的,并在训练过程中通过反向传播算法进行优化。# config.n_vocab# 表示词汇表的大小,即输入文本中不同单词的数量。# config.embed_size# 表示嵌入向量的维度,即每个单词表示为一个向量的长度。# padding_idx = config.n_vocab - 1# 表示填充位置的索引,通常用于处理序列长度不一致的情况。这里将最后一个单词的索引设置为填充位置。self.embeding = nn.Embedding(config.n_vocab,config.embed_size,padding_idx=config.n_vocab - 1)# 定义STML层 模型可以对输入的序列数据进行建模和处理,捕捉序列中的长期依赖关系。# LSTM 层在训练过程中会学习到如何根据输入的序列数据生成隐藏状态,并在后续的计算中使用这些隐藏状态。# 输入为词嵌入层的输出,即词向量化之后的结果self.lstm = nn.LSTM(config.embed_size,config.hidden_size, # 隐藏层节点的数量config.num_layers, # 使用几个LSTMbidirectional=True, # 双向STMLbatch_first=True, # 表示输入数据的批次维度位于第一个位置dropout=config.dropout) # 随机抑制一些节点,防止过拟合# 定义卷积网络# 是一个实例变量,用于创建一个最大池化层(MaxPool1d)。最大池化层用于对输入数据进行下采样,减少特征图的尺寸。self.maxPool = nn.MaxPool1d(config.pad_size) # config.pad_size表示池化操作中的填充大小,用于处理输入序列长度不一致的情况。# 定义fc层即线性层 用于创建一个全连接层(Linear)。全连接层用于将之前的特征映射转换为分类输出。self.fc = nn.Linear(config.hidden_size * 2 + config.embed_size,# 表示全连接层的输入维度,由 LSTM 层的隐藏状态维度乘以 2(因为是双向 LSTM)加上词嵌入层的输出维度组成。config.num_classes) # 表示分类任务的类别数量,即全连接层的输出维度。# Softmax 层用于将全连接层的输出转换为概率分布,表示每个类别的预测概率。self.softmax = nn.Softmax(dim=1)# 模型的前向传播方法,它描述了数据在模型中的流动过程def forward(self, x):# 输出结果[batch_size,seqlen,embed_size]embed = self.embeding(x) # 将输入通过嵌入层得到密集表示out, _ = self.lstm(embed) # 将嵌入的输入通过LSTM层得到输出序列out = torch.cat((embed, out), 2) # 沿特征维度连接嵌入的输入和LSTM输出out = F.relu(out) # 应用ReLU激活函数out = out.permute(0, 2, 1) # 调整张量的维度以进行最大池化操作out = self.maxPool(out).reshape(out.size()[0], -1) # 执行最大池化操作,获得固定大小的可变长度输入表示out = self.fc(out) # 将池化输出通过全连接层out = self.softmax(out) # 应用softmax激活函数,得到类别概率return outif __name__ == '__main__':from configs import Configcfg = Config()cfg.pad_size = 640 # 这个参数用于指定输入序列的固定长度。model_textcls = Model(config=cfg)# 创建一个输入张量input_tensor,包含从0到639的索引序列,并将其reshape为形状为[1, 640]的张量。这里使用了列表推导式来生成索引序列。input_tensor = torch.tensor([i for i in range(640)]).reshape([1, 640])out_tensor = model_textcls.forward(input_tensor)print(out_tensor.size())print(out_tensor)
run_train.py
import torch
import torch.nn as nn
from torch import optim # 导入优化器
from models import Model # 导入模型
from datasets import data_loader, text_CLS # 导入数据
from configs import Config # 导入配置# 创建一个配置对象cfg
cfg = Config()# 微博评论数据路径
data_path = "sources/weibo_senti_100k.csv"
# 停用词路径
data_stop_path = "sources/hit_stopword.txt"
# 字典路径
dict_path = "sources/dict"dataset = text_CLS(dict_path, data_path, data_stop_path)
# 使用data_loader函数创建一个训练数据的数据加载器train_dataloader,用于按批次加载数据。
train_dataloader = data_loader(dataset, cfg)# 将配置对象cfg的pad_size属性设置为数据集的最大序列长度。
cfg.pad_size = dataset.max_len_path# 创建一个模型对象model_text_cls,并将其移动到指定的设备(GPU或CPU)上进行计算。
model_text_cls = Model(config=cfg)
model_text_cls.to(cfg.devices)# 定义损失函数 采用交叉熵
loss_func = nn.CrossEntropyLoss()
# 定义优化器
optimizer = optim.Adam(model_text_cls.parameters(), lr=cfg.learn_rate)flag = 1
# 开始训练
"""
使用嵌套的循环进行模型的训练。外层循环迭代训练的轮数(numepochs),内层循环迭代每个批次的数据。
在每个批次中,将数据和标签转换为张量并移动到指定的设备上。然后,将模型的梯度清零(zero_grad()),
通过调用模型的前向传播方法(forward())获取预测结果,并计算损失值。
接着,通过调用损失函数的backward()方法进行反向传播,计算梯度并更新模型的参数(step())。
"""
for epoch in range(cfg.numepochs):for i, batch in enumerate(train_dataloader): # 使用enumerate函数获取批次的索引i和对应的数据batch。label, data = batch# 将数据和标签转换为张量,并将它们移动到指定的设备上(GPU或CPU)。data = torch.tensor(data).to(cfg.devices)label = torch.tensor(label, dtype=torch.int64).to(cfg.devices)optimizer.zero_grad() # 将优化器的梯度缓冲区清零,以便进行下一次反向传播。pred = model_text_cls.forward(data) # 通过调用模型的前向传播方法,将数据输入模型获得预测结果。loss_val = loss_func(pred, label) # 使用损失函数计算预测结果与标签之间的损失值。# 打印预测结果# print(pred)# print(label)# print("epoch is {} ,ite is {},val is {}".format(epoch, i, loss_val))loss_val.backward() # 执行反向传播,计算梯度。optimizer.step() # 根据计算得到的梯度,更新模型的参数。print(f"完成{flag}次迭代{epoch}")flag += 1if epoch % 20 == 0:print("保存了{}".format(epoch))torch.save(model_text_cls.state_dict(), "models/{}.pth".format(epoch))print("训练完成")
test.py
import torch
import torch.nn as nn
from torch import optim # 导入优化器
from models import Model # 导入模型
from datasets import data_loader, text_CLS # 导入数据
from configs import Config # 导入配置
#创建一个配置对象cfg
cfg = Config()# 微博评论数据路径
data_path = "sources/weibo_senti_100k.csv"
# 停用词路径
data_stop_path = "sources/hit_stopword.txt"
# 字典路径
dict_path = "sources/dict"dataset = text_CLS(dict_path, data_path, data_stop_path)
#使用data_loader函数创建一个训练数据的数据加载器train_dataloader,用于按批次加载数据。
train_dataloader = data_loader(dataset, cfg)#将配置对象cfg的pad_size属性设置为数据集的最大序列长度。
cfg.pad_size = dataset.max_len_path# 创建一个模型对象model_text_cls,并将其移动到指定的设备(GPU或CPU)上进行计算。
model_text_cls = Model(config=cfg)
model_text_cls.to(cfg.devices)model_text_cls.load_state_dict(torch.load("models"))# 开始训练
"""
使用嵌套的循环进行模型的训练。外层循环迭代训练的轮数(numepochs),内层循环迭代每个批次的数据。
在每个批次中,将数据和标签转换为张量并移动到指定的设备上。然后,将模型的梯度清零(zero_grad()),
通过调用模型的前向传播方法(forward())获取预测结果,并计算损失值。
接着,通过调用损失函数的backward()方法进行反向传播,计算梯度并更新模型的参数(step())。
"""for i, batch in enumerate(train_dataloader): #使用enumerate函数获取批次的索引i和对应的数据batch。label, data = batch#将数据和标签转换为张量,并将它们移动到指定的设备上(GPU或CPU)。data = torch.tensor(data).to(cfg.devices)label = torch.tensor(label, dtype=torch.int64).to(cfg.devices)pred_softmax = model_text_cls.forward(data) #通过调用模型的前向传播方法,将数据输入模型获得预测结果。# 打印预测结果# print(pred_softmax)# print(label)# print("epoch is {} ,ite is {},val is {}".format(epoch, i, loss_val))pred=torch.argmax(pred_softmax,dim=1)# print(pred)out= torch.eq(pred,label)# print(out)print(out.sum()*1.0/pred.size()[0])