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
posted @ 2023-12-07 09:15  idazhi  阅读(38)  评论(0编辑  收藏  举报