python实战(六)——推特文本分类

一、任务目标

        这次我们用的是kaggle的入门数据集《Natural Language Processing with Disaster Tweets》,为了便于评估建模效果,我们仅使用带标签的train.csv文件。这个任务的目标是根据给出的推特文本判断是否真的是发生了灾难,这是由于一些人会使用与灾难相关的词语去描述正在发生的事情,但却并不是真的发生了灾难。比如“大地震!xxx发布了最新的自研操作系统!”,这里用到了地震这个词,但实际上只是表达作者对此的震惊,并不是真的地震了。我们需要训练一个模型去划分哪些是真的灾难,哪些只是夸张的表达(二分类问题)

二、深度学习建模

1、探索性数据分析

        首先看看数据量和各个列:

import pandas as pd
df = pd.read_csv('./data/disasterTweets.csv')
print('数据量:', len(df))
print(df.head())

        打印一下各列的信息:

print(df.info())

        我们可以看到总共有5列,其中最主要的是text和target列,同时,keyword和location列存在一定数量的空值。

2、数据预处理

        由于keyword和location列存在一些空值,这里我们需要思考处理的策略。假设我们仅使用text和target进行建模,那么大可直接去掉keyword和location两列。然而,凭直觉判断,或许这两列能够为建模提供一些额外的信息,比如可能在哪个地方发生了真实的灾难,推特文本中也会带上这个地点名,又或者关键词可以帮助我们判断到底推文的重点的地方在哪里,那么我们就不能简单的删掉这两列又或是删掉包含空值的行(因为要预测的新数据或许这两个字段就是空的,我们需要考虑这种情况的处理,总不能把要预测的新数据中的这种含空值列也删掉)

        在这里,笔者选择使用文本填充空值(原字段数据类型就是文本):

df = df.fillna('EMPTY')

3、特征工程

(1)处理location列

        这里我们考虑构造一些新的特征,因为我们打算充分使用keyword和location两列。首先处理location,我们统计一下,location是否在推特正文中出现过:

loc_in_text = []
for loc, twe in zip(df['location'], df['text']):
    if loc in twe:
        loc_in_text.append(1)
    else:
        loc_in_text.append(0)
df['loc_in_text'] = loc_in_text

        接着,我们看一下在正文中出现了location且实际发生灾难的比例:

have_disaster = 0
no_disaster = 0
for lab, lit in zip(df['target'], df['loc_in_text']):
    if lab==lit:
        if lab==0:
            no_disaster += 1
        else:
            have_disaster += 1
print('没有发生灾难的数据量:{},文中出现location的数据量:{}'.format(df['target'].tolist().count(0), no_disaster))
print('发生灾难的数据量:{},文中出现location的数据量:{}'.format(df['target'].tolist().count(1), have_disaster))

        可以看到,虽然实际发生灾难的推文中出现location的推文只有数十条,但是在没发生灾难的推文中大部分都出现了location,可见location应该有一定用处。

(2)处理keyword列

        这里我们如法炮制:

keyword_in_text = []
for kw, twe in zip(df['keyword'], df['text']):
    if kw in twe:
        keyword_in_text.append(1)
    else:
        keyword_in_text.append(0)
df['keyword_in_text'] = keyword_in_text

have_keyword = 0
no_keyword = 0
for lab, kit in zip(df['target'], df['keyword_in_text']):
    if lab==kit:
        if lab==0:
            no_keyword += 1
        else:
            have_keyword += 1
print('没有发生灾难的数据量:{},文中出现keyword的数据量:{}'.format(df['target'].tolist().count(0), no_keyword))
print('发生灾难的数据量:{},文中出现keyword的数据量:{}'.format(df['target'].tolist().count(1), have_keyword))

        可见,keyword在两类数据中的出现概率差距要比location小,但是我们目前先保留。

4、文本表示

        为了对比加入location和keyword辅助信息与否对建模结果的影响,这里我们分两种情况进行文本表示。同时,本文直接应用bert模型进行文本向量化,不对bert进行任务微调,展示一下预训练语言模型拿来即用的效果。首先加载模型:

from transformers import BertTokenizer, BertModel
import torch
 
# 同样,可以去HuggingFace找更强大的模型
model_name = 'bert-base-uncased'
# 初始化分词器
tokenizer = BertTokenizer.from_pretrained(model_name)
# 加载模型
model = BertModel.from_pretrained(model_name)

(1)原始推文文本表示

        由于数据量不大,这里不使用DataLoader了,直接写循环:

texts = df['text'].tolist()
# 定义批量大小
batch_size = 16
# 创建一个空列表来存储所有的句子嵌入
all_sentence_embeddings = []
# 处理数据的批量
for i in range(0, len(texts), batch_size):
    if i%100==0:
        print('processing No.{} sample'.format(i))
        
    batch_texts = texts[i:i+batch_size]
    
    # 分词器生成Bert必要的输出格式
    inputs = tokenizer(batch_texts, padding=True, truncation=True, return_tensors="pt")
    
    # 禁用梯度,打开预测模式
    with torch.no_grad():
        # 预测输出
        outputs = model(**inputs)
    
    # 取最后一层的token平均向量
    sentence_embeddings = outputs.pooler_output
    
    # 将当前批量的句子嵌入添加到列表中
    all_sentence_embeddings += sentence_embeddings

(2)原始推文加辅助信息文本表示

        首先,文本类信息无法像数值类数据建模那样使用简单的0、1索引,我们选择通过在原推文添加额外文本的形式将附加的location和keyword信息融入进文本中(某种程度上来看,这属于语义增强的一种方式)。下面这里只是一个示例,实际上可以有很多种方式的语义增强,得益于预训练模型强大的理解能力,我们所增加的文本信息能够较好地被模型理解并表达出来,这是传统文本表示模型所无法比拟的。

texts = df['text'].tolist()
location_info = '. With location:'
keyword_info = 'With keyword:'
texts_with_tips = []
for text, loc, kw, word in zip(texts, loc_in_text, keyword_in_text, df['keyword'].tolist()):
    if loc==1:
        text += location_info+'True.'
    else:
        text += location_info+'False.'
    if kw==1:
        text += keyword_info+word+'True.'+'Keyword:'+word+'.'
    else:
        text += keyword_info+'False.'
    texts_with_tips.append(text)
texts = texts_with_tips
# 定义批量大小
batch_size = 16
# 创建一个空列表来存储所有的句子嵌入
all_sentence_embeddings = []
# 处理数据的批量
for i in range(0, len(texts), batch_size):
    if i%100==0:
        print('processing No.{} sample'.format(i))
        
    batch_texts = texts[i:i+batch_size]
    
    # 分词器生成Bert必要的输出格式
    inputs = tokenizer(batch_texts, padding=True, truncation=True, return_tensors="pt")
    
    # 禁用梯度,打开预测模式
    with torch.no_grad():
        # 预测输出
        outputs = model(**inputs)
    
    # 取最后一层的token平均向量
    sentence_embeddings = outputs.pooler_output
    
    # 将当前批量的句子嵌入添加到列表中
    all_sentence_embeddings += sentence_embeddings

5、分类建模

        先划分数据集,sklearn模型无法处理torch的tensor,我们将它转换成list类型:

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split([list(li) for li in all_sentence_embeddings], df['target'].tolist(), stratify=df['target'].tolist(), test_size=0.3, random_state=2024)

        这里下游模型直接使用一个简单的MLP:

from sklearn.neural_network import MLPClassifier
from sklearn.metrics import precision_score, recall_score, f1_score
dtc = MLPClassifier(hidden_layer_sizes=(128,64), max_iter=100, random_state=2024)
dtc.fit(X_train, y_train)
y_pred = dtc.predict(X_test)
print('Prec:', precision_score(y_test, y_pred))
print('Rec:', recall_score(y_test, y_pred))
print('F1:', f1_score(y_test, y_pred))

(1)原始推文文本表示建模结果

(2)语义增强后的推文文本表示建模结果

        显然,增加了语义信息之后,从F1值的角度来看,一定程度上提升了模型性能(recall提升了5个点,但precision降低了5个点)。当然这个结果并不算得上好,可以进一步通过优化语义增强方式、微调Bert或者调整下游模型来提升整体模型的预测能力(具体可参考kaggle上的开源方案)。

三、完整代码

        

import pandas as pd
from transformers import BertTokenizer, BertModel
from sklearn.model_selection import train_test_split
from sklearn.neural_network import MLPClassifier
from sklearn.metrics import precision_score, recall_score, f1_score
import torch
#设置显示窗口数据显示完整
pd.set_option('display.max_rows', 500)
pd.set_option('display.max_columns', 100)
pd.set_option('display.width', 1000)


# 读取数据
df = pd.read_csv('./data/disasterTweets.csv')
print('数据量:', len(df))
print(df.head())
print(df.info())
df = df.fillna('EMPTY')

# 统计位置特征在推文中的出现次数
loc_in_text = []
for loc, twe in zip(df['location'], df['text']):
    if loc in twe:
        loc_in_text.append(1)
    else:
        loc_in_text.append(0)
df['loc_in_text'] = loc_in_text

have_disaster = 0
no_disaster = 0
for lab, lit in zip(df['target'], df['loc_in_text']):
    if lab==lit:
        if lab==0:
            no_disaster += 1
        else:
            have_disaster += 1
print('没有发生灾难的数据量:{},文中出现location的数据量:{}'.format(df['target'].tolist().count(0), no_disaster))
print('发生灾难的数据量:{},文中出现location的数据量:{}'.format(df['target'].tolist().count(1), have_disaster))

# 统计关键词特征在推文中的出现次数
keyword_in_text = []
for kw, twe in zip(df['keyword'], df['text']):
    if kw in twe:
        keyword_in_text.append(1)
    else:
        keyword_in_text.append(0)
df['keyword_in_text'] = keyword_in_text

have_keyword = 0
no_keyword = 0
for lab, kit in zip(df['target'], df['keyword_in_text']):
    if lab==kit:
        if lab==0:
            no_keyword += 1
        else:
            have_keyword += 1
print('没有发生灾难的数据量:{},文中出现keyword的数据量:{}'.format(df['target'].tolist().count(0), no_keyword))
print('发生灾难的数据量:{},文中出现keyword的数据量:{}'.format(df['target'].tolist().count(1), have_keyword))


# 定义模型权重,可以去HuggingFace找更强大的模型
model_name = 'bert-base-uncased'
# 初始化分词器
tokenizer = BertTokenizer.from_pretrained(model_name)
# 加载模型
model = BertModel.from_pretrained(model_name)

# texts = df['text'].tolist()
texts = df['text'].tolist()
location_info = '. With location:'
keyword_info = 'With keyword:'
texts_with_tips = []
for text, loc, kw, word in zip(texts, loc_in_text, keyword_in_text, df['keyword'].tolist()):
    if loc==1:
        text += location_info+'True.'
    else:
        text += location_info+'False.'
    if kw==1:
        text += keyword_info+word+'True.'+'Keyword:'+word+'.'
    else:
        text += keyword_info+'False.'
    texts_with_tips.append(text)
texts = texts_with_tips

# 定义批量大小
batch_size = 16
# 创建一个空列表来存储所有的句子嵌入
all_sentence_embeddings = []
# 处理数据的批量
for i in range(0, len(texts), batch_size):
    if i % 100 == 0:
        print('processing No.{} sample'.format(i))

    batch_texts = texts[i:i + batch_size]

    # 分词器生成Bert必要的输出格式
    inputs = tokenizer(batch_texts, padding=True, truncation=True, return_tensors="pt")

    # 禁用梯度,打开预测模式
    with torch.no_grad():
        # 预测输出
        outputs = model(**inputs)

    # 取最后一层的token平均向量
    sentence_embeddings = outputs.pooler_output

    # 将当前批量的句子嵌入添加到列表中
    all_sentence_embeddings += sentence_embeddings

# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split([list(li) for li in all_sentence_embeddings],
                                                    df['target'].tolist(), stratify=df['target'].tolist(), test_size=0.3, random_state=2024)

# 训练MLP
dtc = MLPClassifier(hidden_layer_sizes=(128,64), max_iter=100, random_state=2024)
dtc.fit(X_train, y_train)
y_pred = dtc.predict(X_test)
print('Prec:', precision_score(y_test, y_pred))
print('Rec:', recall_score(y_test, y_pred))
print('F1:', f1_score(y_test, y_pred))

四、总结

        本文使用推特分类数据集展示了文本数据的分类建模过程,其中使用了Bert预训练语言模型进行文本向量化,并训练一个MLP神经网络进行文本分类。从结果中可以看到,即便是未经过微调,Bert模型仍然表现出了较为强大的文本表示能力,同时使用一定的辅助信息进行语义增强也能在一定程度上提升分类性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值