rnn完成帖子分类
数据
使用的数据来自某高校的论坛,使用爬虫爬取两个模块
爬虫使用requests库发送HTTPS请求,爬取上述两个板块各80页数据,包含3000个帖子,再使用BeautifulSoup解析HTML内容,得到帖子标题
import requests
from bs4 import BeautifulSoup
import time
from tqdm import tqdm
import requests
import time
from tqdm import tqdm
fid = 735 # 目标板块的ID
titles735 = [] # 存放爬取的数据
for pid in tqdm(range(1, 80)):
r = requests.get('https://www.XXX.com/forumdisplay.php?fid=%d&page=%d' % (fid, pid))
with open(‘raw_data/%d-%d.html' % (fid, pid), 'wb') as f: # 原始 HTML 写入文件
f.write(r.content)
b = BeautifulSoup(r.text)
table = b.find('table', id='forum_%d' % fid) # 寻找返回的HTML中的table 标签
trs = table.find_all('tr')
for tr in trs[1:]:
title = tr.find_all('a')[1].text # 获取 a 标签中的文字
titles735.append(title)
time.sleep(1) # 阻塞一秒,防止过快的请求给网站服务器造成压力
with open('%d.txt' % fid, 'w', encoding='utf8') as f: # 把数据写入文件
for l in titles735:
f.write(l + '\n')
fid = 644
titles644 = []
for pid in tqdm(range(1, 80)):
r = requests.get('https://www.XXX.com/forumdisplay.php?fid=%d&page=%d' % (fid, pid))
with open(‘raw_data/%d-%d.html' % (fid, pid), 'wb') as f: # 原始HTML写入文件
f.write(r.content)
b = BeautifulSoup(r.text)
table = b.find('table', id='forum_%d' % fid)
trs = table.find_all('tr')
for tr in trs[1:]:
title = tr.find_all('a')[1].text
titles644.append(title)
time.sleep(1)
with open('%d.txt' % fid, 'w', encoding='utf8') as f:
for l in titles644:
f.write(l + '\n')
读取已经爬取好的文件,并解析HTML内容
import time
from tqdm import tqdm
fid = 735
titles735 = []
for pid in tqdm(range(1, 80)):
with open(' raw_data /%d-%d.html' % (fid, pid), 'r', encoding='utf8') as f: # 需选
择正确编码
b = BeautifulSoup(f.read())
table = b.find('table', id='forum_%d' % fid)
trs = table.find_all('tr')
for tr in trs[1:]:
title = tr.find_all('a')[1].text
titles735.append(title)
with open('%d.txt' % fid, 'w', encoding='utf8') as f:
for l in titles735:
f.write(l + '\n')
fid = 644
titles644 = []
for pid in tqdm(range(1, 80)):
with open('raw_data/%d-%d.html' % (fid, pid), 'r', encoding='utf8') as f:
b = BeautifulSoup(f.read())
b = BeautifulSoup(r.text)
table = b.find('table', id='forum_%d' % fid)
trs = table.find_all('tr')
for tr in trs[1:]:
title = tr.find_all('a')[1].text
titles644.append(title)
with open('%d.txt' % fid, 'w', encoding='utf8') as f:
for l in titles644:
f.write(l + '\n')
读取数据
# 定义两个列表分别存储两个板块的帖子数据
# academy_titles 考研考博 job_titles 招聘信息
# 定义两个list分别存放两个板块的帖子数据
academy_titles = []
job_titles = []
with open('E:/nlp/dataSet/academy_titles.txt', encoding='utf8') as f:
for l in f: # 按行读取文件
academy_titles.append(l.strip( )) # strip 方法用于去掉行尾空格
with open('E:/nlp/dataSet/job_titles.txt', encoding='utf8') as f:
for l in f: # 按行读取文件
job_titles.append(l.strip()) # strip 方法用于去掉行尾空格
输入和输出
统计数据集中出现的字符数量
不论是使用词嵌入还是one-hot表示法都需要现实的数据集中一共出现了多少个不同的字符
# 使用set函数去重
char_set = set() # 创建集合 集合可自动去除重复的元素
for title in academy_titles: # 遍历考研考博模块的的所有标题
for ch in title: # 遍历标题中每个字符
char_set.add(ch) # 把字符加入到集合中
for title in job_titles:
for ch in title:
char_set.add(ch)
print(len(char_set))
使用one-hot编码表示标题数据
此处只介绍使用one-hot表示法,但是后续使用的是词嵌入因为数据中的字符量太多,使用one-hot表示法效率很低。
# 把一个标题字符串转换为张量
import torch
char_list = list(char_set)
n_chars = len(char_list) + 1 # 加一个 UNK
def title_to_tensor(title):
tensor = torch.zeros(len(title), l,n_chars)
for li, ch in enumerate(title):
try:
ind = char_list.index(ch)
except ValueError:
ind = n_chars - 1
tensor[li][0][ind] = l
return tensor
# 把前面代码char_set转换为列表,因为集合数据结构插入快,且元素无重复,适合用于统计个数,但集合无法根据下标访问字符所有要转换列表。
使用嵌入表示标题数据
# 把标题字符对应的id组成张量即可
import torch
char_list = list(char_set)
n_chars = len(char_list) + 1 # 加一个 UNK
def title_to_tensor(title):
tensor = torch.zeros(len(title), dtype=torch.long)
for li, ch in enumerate(title):
try:
ind = char_list.index(ch)
except ValueError:
ind = n_chars - 1
tensor[li] = ind
return tensor
# 定义embedding
# 第一个词语数量 第二个维度就是经过embedding后输出的词向量的维度 100
embedding = torch.nn.Embedding(n_chars,100) # 第一个词语数量 第二个维度
# 实际使用embedding应该定义在模型中
print(job_titles[1])
print(title_to_tensor(job_titles[1]))
----
招聘兼职/ 笔试考务 /200-300 每人
tensor([ 690, 1472, 1412, 853, 384, 220, 1161, 483, 937, 447, 220, 384,
1530, 1225, 1225, 1558, 548, 1225, 1225, 220, 1542, 1169])
输出
0:考研考博 1招聘信息
模型输出一个0到1的浮点数,可以设定一共阈值 如0.5 若输出大于0.5则认为是分类1小于0.5则认为是分类0
或者可以让模型输出两个值,第一个值代表“考研考博”板块,第二个值代表“招聘信息板块。在这种情况下可以比较两个值的大小,标题字符串属于较大的值对应的分类。这时可使用张量的 topk 方法获取一个张量中最大的元素的值以及它的下标。代码示例如下。
t = torch.tensor([0.3,0.7])
topn,topi = t.topk(1)
print(topn,topi)
----
tensor([0.7000]) tensor([1])
# 第一个值是较大的元素的值0.7000,第二个值1代表该元素的下标,也刚好是我们的分类l。
字符级RNN
定义模型
在模型中定义词嵌入、输入到隐藏层的 Linear 层、输出的Linear层以及 Softmax层。
# word_count词表大小 embedding_size词嵌入维度
# hidden_size隐藏层维度 output_size输出维度
import torch.nn as nn
class RNN(nn.Module):
def __init__(self, word_count, embedding_size, hidden_size, output_size):
super(RNN, self).__init__() # 构造父类的构造函数 初始化模型
self.hidden_size = hidden_size # 保存隐藏层的大小
self.embedding = torch.nn.Embedding(word_count, embedding_size) # 词嵌入
self.i2h = nn.Linear(embedding_size + hidden_size, hidden_size) # 输入到隐藏层
self.i2o = nn.Linear(embedding_size + hidden_size, output_size) # 输入到输出
self.softmax = nn.LogSoftmax(dim=1) # softmax层
def forward(self, input_tensor, hidden):
word_vector = self.embedding(input_tensor) # 把字ID转换为向量
combined = torch.cat((word_vector, hidden), 1)# 拼接向量和隐藏层输出
hidden = self.i2h(combined) #得到隐藏层输出
output = self.i2o(combined) #得到输出
output = self.softmax(output) #得到 Softmax 输出
return output, hidden
def initHidden(self):
return torch.zeros(1, self.hidden_size) #初始使用全0的隐藏层输出
运行模型
测试模型,看模型输入输出的形式。首先定义模型,设置embedding_size为 200,即每个词语用200维向量表示:隐藏层为128 维。模型的输出结果是对两个类别的判断,一个类别表“考研该考博”,另一个代表“招聘信息”。
embedding_size = 100
n_hidden = 128
n_categories = 2
rnn = RNN(n_chars, embedding_size, n_hidden, n_categories)
然后尝试把一个标题转换成向量,再把该向量输入到模型里,并查看模型输出
input_tensor = title_to_tensor(academy_titles[0])
print('input_tensor:\n', input_tensor)
----
input_tensor:
tensor([ 273, 1510, 247, 499, 132, 1191, 952, 692, 1478, 1569, 745, 760,
1191, 210, 1440, 827, 1215, 225, 1406, 1068, 1352])
hidden = rnn.initHidden()
output, hidden = rnn(input_tensor[0].unsqueeze(dim=0), hidden)
print('output:\n', output)
print('hidden:\n', hidden)
print('size of hidden:\n', hidden.size())
----
在一段文本上运行 RNN模型需要使用循环,初始隐藏层输入用全零向量,每次循环传一个字符进模型,舍弃中间的模型输出,但是保存隐藏层输出。这个过程的代码如下。
def run_rnn(rnn, input_tensor):
hidden = rnn.initHidden()
for i in range(input_tensor.size()[0]):
output, hidden = rnn(input_tensor[i].unsqueeze(dim=0), hidden)
return output
第一个参数rnn 是模型对象,第二个参数 input_tensor 是输入的向量,可以使用 title_to_tensor函数得到。run 函数中的for 循环遍历输入向量,完成上述RNN执行过程,然后返回最后一个字符的模型输出。
数据预处理
为了方便训练和评估模型,我们可以先把数据划分为训练集和测试集,并根据需要打乱数据的顺序。另外可以预先调整数据的格式,添加标签,便于使用。PyTorch 提供了 Dataset 类和DataLoader 类简化数据处理的过程,但是本节将使用自定义的方法完成该过程
合并数据并添加数据
all_data = []#定义新的列表用于保存全部数据
categories = ["考研考博", "招聘信息"]# 定义两个类别,下标0代表“考研考博”,1代表“招聘信
for l in academy_titles: # 把“考研考博”的帖子标题加入到all_data中,并添加标签为0
all_data.append((title_to_tensor(l), torch.tensor([0], dtype=torch.long)))
for l in job_titles:#把“招聘信息”的帖子标题加入到all_data中,并添加标签为1
all_data.append((title_to_tensor(l), torch.tensor([1], dtype=torch.long)))
划分训练集和数据集
可以使用 Python的random 库中的 shufle函数将all data打乱顺序,并按比例划分训练和测试集。这里将70%的数据作为训练集,30%的数据作为测试集。sklearn 库中有函数可以现打乱数据顺序并切分数组的功能
import random
random.shuffle(all_data) # 打乱数组元素顺序
data_len = len(all_data) # 数据条数
split_ratio = 0.7 #训练集数据占比
train_data = all_data[:int(data_len*split_ratio)] #切分数组
test_data = all_data[int(data_len*split_ratio):] #打印训练集和测试集长度
print("Train data size: ", len(train_data))
print("Test data size: ", len(test_data))
---
Train data size: 4975
Test data size: 2133
训练和评估
赞聚如练是根报训练数据和标签不断调整模型参数的过程:模型评估则是使用模型预测测试集结果,并与真实标签比对的过程。
训练
模型训练中,首先需要一个损失函数,用于评估模型输出与实际的标签之间的差距,然后基于这个差距来决定如何更新模型中每个参数。其次需要学习率,用于控制每次更新参数的速度。这里使用 NLLLoss 函数,它可以方便地处理多分类问题
def train(rnn, criterion, input_tensor, category_tensor):
rnn.zero_grad() # 重置梯度
output = run_rnn(rnn, input_tensor) #运行模型,并获取输出
loss = criterion(output, category_tensor) # 计算损失
loss.backward() # 反向传播
# 根据梯度更新模型的参数
for p in rnn.parameters():
p.data.add_(p.grad.data, alpha=-learning_rate)
return output, loss.item()
train函数的第一个参数mn是模型对象;第二个参数criterion 是损失函数,第三个参数nput tensor 是输入的标题对应的张量;第四个参数category_tensor 则是这个标题对应的分类,也是张量。
评估
模型评估是至关重要的,可了解模型的效果。有很多指标可以衡量模型的效果,比如训练过程中的损失,模型在一个数据集上的损失越小,说明模型对这个数据集的拟合程度越好;准确率(accuracy),表示在测试模型的过程中,模型正确分类的数据占全部测试数据的比例。这里采用准确率来评估模型,
def evaluate(rnn, input_tensor):
with torch.no_grad():
hidden = rnn.initHidden()
output = run_rnn(rnn, input_tensor)
return output
这里使用torchnograd 函数,对于该函数中的内容 PyTorch 不会执行梯度计算,所以计算速度会更快。
训练模型
from tqdm import tqdm
epoch = 1 # 训练轮数
embedding_size = 200
n_hidden = 10
n_categories = 2
learning_rate = 0.005 # 学习率
rnn = RNN(n_chars, embedding_size, n_hidden, n_categories)
criterion = nn.NLLLoss() #损失函数
loss_sum = 0 #当前损失累加
all_losses = [] #记录训练过程中的损失变化,用于绘制损失变化图
plot_every = 100 #每多少个数据记录一次平均损失
for e in range(epoch): #进行epoch 轮训练(这里只有一轮)
for ind, (title_tensor, label) in enumerate(tqdm(train_data)): #遍历训练集中每个数据
output, loss = train(rnn, criterion, title_tensor, label)
loss_sum += loss
if ind % plot_every == 0:
all_losses.append(loss_sum / plot_every)
loss_sum = 0
c = 0
for title, category in tqdm(test_data):
output = evaluate(rnn, title)
topn, topi = output.topk(1)
if topi.item() == category[0].item():
c += 1
print('accuracy', c / len(test_data))
首先我们引入 tqdm库,这个库用来显示模型训练的进度。它能够自动计算并显示当前度百分比、已消耗时间、预估剩余时间、每次循环使用的时间等信息,非常方便,可直接使pip install tqdm命令安装。
第二行代码定义训练轮数,因为模型比较简单,实验中发现仅训练一轮就能达到良好效果所以这里设为1。很多时候可能要较多的轮数才能得到效果良好的模型。
然后定义学习率、损失函数还有用于记录训练过程中的损失的变量,训练完成后可以用些数据绘制损失变化的折线图。
接下来的外层循环代表训练轮次,每轮训练会先遍历数据集,更新模型参数,紧接着遍历测试集,并计算 Accuracy。
c = 0
l1 = []
l2 = []
for title, category in tqdm(test_data):
output = evaluate(rnn, title)
topn, topi = output.topk(1)
l1.append(topi.item())
l2.append(category[0].item())
if topi.item() == category[0].item():
c += 1
print('accuracy', c / len(test_data))
print(l1[:40])
print(l2[:40])
sum(l1)
----
1059
sum(l2)
---
1077
c = 0
for title, category in tqdm(test_data):
output = evaluate(rnn, title)
topn, topi = output.topk(1)
if topi.item() == category[0].item():
c += 1
print('accuracy', c / len(test_data))
绘图
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
plt.figure(figsize=(10,7))
plt.ylabel('Average Loss')
plt.plot(all_losses[1:])
保存模型和加载模型
训练模型往往需要耗费较多的时间并使用大量数据,完成模型的训练和评估之后,我们可以把模型保存为文件,并在需要时再次加载模型。
仅保存模型参数
使用 torch.save函数保存模型参数,即通过模型的 state dict 函数获取模型参数,加载模型时需要先创建该模型的类,然后使用新创建的类加载之前保存的参数,
# 仅保存模型
# 保存模型
torch.save(rnn.state_dict(), 'rnn_parameter.pkl')
# 加载模型
embedding_size = 200
n_hidden = 128
n_categories = 2
rnn = RNN(n_chars,embedding_size,n_hidden,n_categories)
rnn.load_state_dict(torch.load('rnn_parameter.pkl'))
保存模型与参数
使用 torch.save 函数直接保存模型对象,加载时使用 torch.load 函数直接得到模型对象,不需要重新定义模型,
# 保存模型和参数
# 保存模型
torch.save(rnn, 'rnn_model.pkl')
# 加载模型
rnn = torch.load('rnn_model.pkl')
保存词表
保存模型是需要 保存词表,词表实际上是字符与id的对应的关系,可以使用json包把词表列表村委json文件
# 保存词表
import json
with open('char_list.json', 'w') as f:
json.dump(char_list, f)
# 加载词表
with open('char_list.json', 'r') as f:
char_list = json.load(f)
开发应用
给出任意标题的建议分类
def get_category(title):
title = title_to_tensor(title) # 前面创建的函数
output = evaluate(rnn, title)
topn, topi = output.topk(1)
return categories[topi.item()]
进行测试
def print_test(title):
print('%s\t%s' % (title,get_category(title)))
print_test('考研心得')
print_test('北大实验室博士')
print_test('校招offer比较')
----
考研心得 招聘信息
北大实验室博士 招聘信息
校招offer比较 招聘信息
获取用户输入并返回结果
使用input函数可接受用户输入,类型为字符串。可以把预先训练好的模型保存为文件,
# 保存模型
# torch.save(rnn.state_dict(), 'title_rnn_model.pkl')
torch.save(rnn, 'title_rnn_model.pkl')
import json
import torch
import torch.nn as nn
class RNN(nn.Module):
def __init__(self, word_count, embedding_size, hidden_size, output_size):
super(RNN, self).__init__()
self.hidden_size = hidden_size
self.embedding = torch.nn.Embedding(word_count, embedding_size)
# 输入到隐藏层的转换
self.i2h = nn.Linear(embedding_size + hidden_size, hidden_size)
# 输入到输出的转换
self.i2o = nn.Linear(embedding_size + hidden_size, output_size)
self.softmax = nn.LogSoftmax(dim=1)
def forward(self, input_tensor, hidden):
word_vector = self.embedding(input_tensor)
combined = torch.cat((word_vector, hidden), 1)
hidden = self.i2h(combined)
output = self.i2o(combined)
output = self.softmax(output)
return output, hidden
def initHidden(self):
return torch.zeros(1, self.hidden_size)
embedding_size = 200
n_hidden = 128
n_categories = 2
rnn = RNN(n_chars,embedding_size,n_hidden,n_categories)
rnn.load_state_dict(torch.load('title_rnn_model.pkl'))
# rnn = torch.load('title_rnn_model.pkl') #
# rnn = torch.load('rnn_parameter.pkl')
categories=["考研考博","招聘信息"]
with open('char_list.json','r') as f:
char_list=json.load(f)
n_chars=len(char_list) + 1 #加一个UNK
def title_to_tensor(title):
tensor = torch.zeros(len(title), dtype=torch.long)
for li, ch in enumerate(title):
try:
ind = char_list.index(ch)
except ValueError:
ind = n_chars - 1
tensor[li] = ind
return tensor
def run_rnn(rnn, input_tensor):
hidden = rnn.initHidden()
for i in range(input_tensor.size()[0]):
output, hidden = rnn(input_tensor[i].unsqueeze(dim=0), hidden)
return output
def evaluate(rnn, input_tensor):
with torch.no_grad():
hidden = rnn.initHidden()
output = run_rnn(rnn, input_tensor)
return output
def get_category(title):
title = title_to_tensor(title)
output = evaluate(rnn, title)
topn, topi = output.topk(1)
return categories[topi.item()]
if __name__ == '__main__':
while True:
title=input()
if not title:
break
print(get_category(title))
开发web界面
把上面的代码倒数第六行及之后的代码替换成以下代码即可实现
if __name__ == '__main__':
import flask
app = flask.Flask(__name__) # 创建Flask应用对象
# @app.route('/') C:\Users\dazhi
@app.route('/') # 绑定web服务的/路径
def index():
title =flask.request.values.get('title') # 从HTTP请求的 GET 参数中获取 key为“title”的参数
if title: #获取到正确的参数则调用模型进行分类
return get_category(title)
else: #如果没有获取到参数或者参数为空
return "<form><input name='title' type='text'><input type='submit'></form>"
app.run(host='0.0.0.0',port=12345)
-----
# 访问下面两个其中的一个都行
* Running on http://127.0.0.1:12345
* Running on http://10.8.143.63:12345