• Kaggle竞赛 Real or Not? NLP with Disaster Tweets 文本分类



    前言

    在这场竞赛中,你面临的挑战是建立一个机器学习模型,预测哪些推文是关于真实灾难的,哪些不是。你将有权访问由 10,000 条手动分类的推文组成的数据集。

    一、比赛介绍

    来源kaggle竞赛- Natural Language Processing with Disaster Tweets
    任务:我们需要根据文章的location、keyword以及文本text来判断这篇推文是否和灾难有关,相关部门可以据此来第一时间发现可能存在的灾难并且迅速做出反应,将损失降低到最低。
    在这里插入图片描述

    二、解决方案(探索式资料分析&清洗数据)

    2-1、介绍和引言

    导入库、读取数据、分析

    import re
    import string
    import operator
    from collections import defaultdict
    
    import matplotlib.pyplot as plt
    import seaborn as sns
    
    from sklearn.model_selection import StratifiedKFold, StratifiedShuffleSplit
    from sklearn.metrics import precision_score, recall_score, f1_score
    
    df_train = pd.read_csv('/kaggle/input/nlp-getting-started/train.csv')
    df_test = pd.read_csv('/kaggle/input/nlp-getting-started/test.csv')
    
    print(f'Training Set Shape: {df_train.shape}')
    print(f'Test Set Shape: {df_test.shape}')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    数据规模:Training Set Shape: (7613, 5); Test Set Shape: (3263, 4)

    训练集
    在这里插入图片描述
    验证集
    在这里插入图片描述

    2-2、对于特征keyword(关键字)和location(地点)的处理

    一、探索空值的数量

    print(df_train['location'].isnull().sum())
    print(df_train['keyword'].isnull().sum())
    
    • 1
    • 2

    输出
    2533 61

    二、缺失值的处理: 使用空字符串 ‘’ 来替代缺失值,将缺失值用空字符串填充。

    missing_cols = ['keyword', 'location']
    
    fig, axes = plt.subplots(ncols=2, figsize=(17, 4), dpi=100)
    
    sns.barplot(x=df_train[missing_cols].isnull().sum().index, y=df_train[missing_cols].isnull().sum().values, ax=axes[0])
    sns.barplot(x=df_test[missing_cols].isnull().sum().index, y=df_test[missing_cols].isnull().sum().values, ax=axes[1])
    
    axes[0].set_ylabel('Missing Value Count')
    axes[0].set_title('Training Set')
    axes[1].set_title('Test Set')
    
    plt.show()
    
    for df in [df_train, df_test]:
        for col in ['keyword', 'location']:
            # df[col] = df[col].fillna(f'no_{col}')
            # 在该方法中,使用空字符串 '' 来替代缺失值,将缺失值用空字符串填充。
            df[col] = df[col].fillna('')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    输出:如图输出所示,训练集和测试集的loaction字段缺失较多。
    在这里插入图片描述

    三、分布探索: 输出其唯一的值的数量;

    print(f"Number of unique values in keyword = {df_train['keyword'].nunique()} (Training) - {df_test['keyword'].nunique()} (Test)")
    print(f"Number of unique values in location = {df_train['location'].nunique()} (Training) - {df_test['location'].nunique()} (Test)")
    
    • 1
    • 2

    输出
    Number of unique values in keyword = 221 (Training) - 221 (Test)
    Number of unique values in location = 3341 (Training) - 1602 (Test)

    四、关键字分布探索

    # 以keyword来分组抽取target的平均值
    df_train['target_mean'] = df_train.groupby('keyword')['target'].transform('mean')
    
    fig = plt.figure(figsize=(8,72), dpi=100)
    
    # 绘制图形,根据关键字keyword,并且根据target_mean降序排列
    sns.countplot(y=df_train.sort_values(by='target_mean', ascending=False)['keyword'], hue=df_train.sort_values(by='target_mean', ascending=False)['target'])
    
    plt.legend(loc=1)
    plt.title('Target Distribution in Keywords')
    
    plt.show()
    
    # 删除掉列
    df_train.drop(columns=['target_mean'], inplace=True)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    部分输出:从图中我们可以得知,keyword与label之间有可见的相关性,有些词只在灾难推文中出现,而有些词只在非灾难推文中出现。
    在这里插入图片描述

    看一下上边输出添加了target_mean的df是什么样子: df_train.sort_values(by=‘target_mean’, ascending=False)

    在这里插入图片描述

    2-3、组成新特征

    一、组成新特征:单词数量、唯一的单词数量、停用词数量、url数量、单词平均长度、字符串长度、标点符号数量、#数量、@数量。(#开头的一般是标签,即考虑标签数量,@一般是提及什么朋友,即提到的人数量。)

    # word_count
    df_train['word_count'] = df_train['text'].apply(lambda x:len(str(x).split()))
    df_test['word_count'] = df_test['text'].apply(lambda x:len(str(x).split()))
    
    # unique_word_count
    df_train['unique_word_count'] = df_train['text'].apply(lambda x:len(set(str(x).split())))
    df_test['unique_word_count'] = df_test['text'].apply(lambda x:len(set(str(x).split())))
    
    # stop_word_count
    from wordcloud import STOPWORDS
    df_train['stop_word_count'] = df_train['text'].apply(lambda x: len([w for w in str(x).lower().split() if w in STOPWORDS]))
    df_test['stop_word_count'] = df_test['text'].apply(lambda x: len([w for w in str(x).lower().split() if w in STOPWORDS]))
    
    # url_count
    df_train['url_count'] = df_train['text'].apply(lambda x: len([w for w in str(x).lower().split() if 'http' in w or 'https' in w]))
    df_test['url_count'] = df_test['text'].apply(lambda x: len([w for w in str(x).lower().split() if 'http' in w or 'https' in w]))
    
    # mean_word_length
    df_train['mean_word_length'] = df_train['text'].apply(lambda x: np.mean([len(w) for w in str(x).split()]))
    df_test['mean_word_length'] = df_test['text'].apply(lambda x: np.mean([len(w) for w in str(x).split()]))
    
    # char_count
    df_train['char_count'] = df_train['text'].apply(lambda x: len(str(x)))
    df_test['char_count'] = df_test['text'].apply(lambda x: len(str(x)))
    
    # punctuation_count
    df_train['punctuation_count'] = df_train['text'].apply(lambda x:len([c for c in str(x) if c in string.punctuation]))
    df_test['punctuation_count'] = df_test['text'].apply(lambda x:len([c for c in str(x) if c in string.punctuation]))
    
    # hashtag_count
    df_train['hashtag_count'] = df_train['text'].apply(lambda x: len([c for c in str(x) if c == '#']))
    df_test['hashtag_count'] = df_test['text'].apply(lambda x: len([c for c in str(x) if c == '#']))
    
    # mention_count
    df_train['mention_count'] = df_train['text'].apply(lambda x: len([c for c in str(x) if c == '@']))
    df_test['mention_count'] = df_test['text'].apply(lambda x: len([c for c in str(x) if c == '@']))
    # 输出df_train
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    输出:看看添加了新特征的df_train什么样子。
    在这里插入图片描述

    二、观察新特征上灾难和不是灾难的分布情况、每个特征训练集和验证集上的分布情况

    meta_features = ['word_count', 'unique_word_count', 'stop_word_count', 'url_count', 'mean_word_length', 'char_count', 'punctuation_count']
    disaster_tweets = df_train['target'] == 1
    
    fig, axes = plt.subplots(ncols=2, nrows=len(meta_features), figsize=(20,40), dpi=100)
    
    for i, feature in enumerate(meta_features):
        sns.distplot(df_train.loc[~disaster_tweets][feature], label='Not Disaster', ax=axes[i][0], color='green')
        sns.distplot(df_train.loc[disaster_tweets][feature], label='Disaster', ax=axes[i][0], color='red')
        sns.distplot(df_train[feature], label='Training', ax=axes[i][1])
        sns.distplot(df_test[feature], label='Test', ax=axes[i][1])
        
        for j in range(2):
            axes[i][j].legend()
        
        axes[i][0].set_title(f'{feature} Target Distribution in Training Set')
        axes[i][1].set_title(f'{feature} Training & Test Set Distribution')
    
    plt.show()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    输出:这里只展示部分图片,根据图片显示,分布大概一致,可以使用,如果出现一些偏差比较大的分布,可以考虑直接砍掉特征,如果是训练集和验证集分布不一致,可以考虑重新整理一下验证集,使得分布大概相同。
    在这里插入图片描述

    2-4、target分布

    target: 画出图表观测一下分布情况。

    fig, axes = plt.subplots(ncols=2, figsize=(17, 4), dpi=100)
    plt.tight_layout()
    
    df_train.groupby('target').count()['id'].plot(kind='pie', ax=axes[0], labels=['Not Disaster (57%)', 'Disaster (43%)'])
    sns.countplot(x=df_train['target'], hue=df_train['target'], ax=axes[1])
    
    axes[0].set_title('Target Distribution in Training Set')
    axes[1].set_title('Target Count in Training Set')
    
    plt.show()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    输出:根据图表可知,0、1标签的数量大致持平,没有相差太大。
    在这里插入图片描述

    2-5、文本清理

    文本清理: 把一些奇奇怪怪的符号都去掉,适用于tweet评论。

    
    
    
    • 1
    • 2

    2-6、错误标签样本处理

    错误标签样本处理: 某些样本的标签是错误的,剔除掉。

    df_mislabeled = df_train.groupby(['text_cleaned']).nunique().sort_values(by='target', ascending=False)
    df_mislabeled = df_mislabeled[df_mislabeled['target'] > 1]['target']
    df_mislabeled.index.tolist()
    
    • 1
    • 2
    • 3

    输出:各位爷,来瞅瞅标错的文本都是什么德行,让我们的标注人员迷失了自己。

    在这里插入图片描述

    把标记错误的老六都剔除掉。

    df_train = df_train[~df_train['text_cleaned'].isin(df_mislabeled.index.tolist())]
    
    • 1

    三、训练模型

    3-1、加载所需包

    import random
    import torch
    from torch.utils.data import TensorDataset, DataLoader, random_split
    from transformers import BertTokenizer
    from transformers import BertForSequenceClassification, AdamW
    from transformers import get_linear_schedule_with_warmup
    
    # 加载huggingface的transformers,编辑词向量。
    # 设定随机种子,使得每次得到的结果一致。
    seed = 42
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    torch.backends.cudnn.deterministic = True
    
    device = torch.device('cuda')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    3-2、数据的进一步处理

    数据的进一步处理: 添加字段,加载预训练分词模型。

    # 添加一个字段,final_text,由keyword和text_cleaned组成
    df_train['final_text'] = df_train['keyword'] + ' ' + df_train['text_cleaned']
    df_test['final_text'] = df_test['keyword'] + ' ' + df_test['text_cleaned']
    
    
    # Get text values and labels
    text_values = df_train['final_text'].values
    labels = df_train['target'].values
    
    # Load the pretrained Tokenizer
    tokenizer = BertTokenizer.from_pretrained('bert-base-uncased', do_lower_case=True)
    
    # 查看编码后的text以及转换为ID的text
    text = '[CLS]'
    print('Original Text : ', text)
    print('Tokenized Text: ', tokenizer.tokenize(text))
    print('Token IDs     : ', tokenizer.convert_tokens_to_ids(tokenizer.tokenize(text)))
    print('\n')
    
    text = '[SEP]'
    print('Original Text : ', text)
    print('Tokenized Text: ', tokenizer.tokenize(text))
    print('Token IDs     : ', tokenizer.convert_tokens_to_ids(tokenizer.tokenize(text)))
    print('\n')
    
    text = '[PAD]'
    print('Original Text : ', text)
    print('Tokenized Text: ', tokenizer.tokenize(text))
    print('Token IDs     : ', tokenizer.convert_tokens_to_ids(tokenizer.tokenize(text)))
    
    # tokenizer的编码(encode)是两个操作步骤的合体
    print(f'Adding Special Tokens Using Encode Func: {tokenizer.encode(text_values[1])}')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    输出
    在这里插入图片描述
    在这里插入图片描述

    3-3、添加特征并绘图

    添加final_text这一列的长度特征并绘图

    # Add a length column which contains the length of the tweet
    # 添加final_text这一列的长度。
    df_train['length'] = df_train['final_text'].apply(len)
    df_test['length'] = df_test['final_text'].apply(len)
    
    # Plot
    sns.set_style('whitegrid', {'axes.grid' : False})
    fig, ax = plt.subplots(1, 2)
    fig.set_size_inches(18, 6)
    
    sns.distplot(df_train['length'], color='#20A387',ax=ax[0])
    sns.distplot(df_test['length'], color='#440154',ax=ax[1])
    
    fig.suptitle("Length of Tweets", fontsize=14)
    ax[0].set_title('Train')
    ax[1].set_title('Test')
    
    plt.show()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在这里插入图片描述

    添加final_text这一列的单词数量并绘图

    # Add column for number of words
    df_train['num_of_words'] = df_train['final_text'].apply(lambda x:len(str(x).split())) 
    df_test['num_of_words'] = df_test['final_text'].apply(lambda x:len(str(x).split())) 
    
    # Plot
    sns.set_style('whitegrid', {'axes.grid' : False})
    fig, ax = plt.subplots(1, 2)
    fig.set_size_inches(18, 6)
    
    sns.distplot(df_train['num_of_words'], color='#20A387',ax=ax[0])
    sns.distplot(df_test['num_of_words'], color='#440154',ax=ax[1])
    
    fig.suptitle("Number of Words in Tweets", fontsize=14)
    ax[0].set_title('Train')
    ax[1].set_title('Test')
    
    plt.show()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在这里插入图片描述

    3-4、训练模型

    处理final_text,返回编码后的ID串。

    def encode_fn(text_list):
        all_input_ids = []
        for text in text_values:
            input_ids = tokenizer.encode(text, add_special_tokens=True, max_length=180, pad_to_max_length=True, return_tensors='pt')
            all_input_ids.append(input_ids)
        all_input_ids = torch.cat(all_input_ids, dim=0)
        return all_input_ids
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    划分数据为训练集和验证集

    epochs = 4
    batch_size = 32
    
    # Split data into train and validation
    # 将数据转化为输入模型需要的数据类型
    all_input_ids = encode_fn(text_values)
    labels = torch.tensor(labels)
    
    # TensorDataset是pytorch中的一个类,用于将多个张量打包成一个数据集,将输入数据张量和标签张量进行一一对应组合。
    # 划分训练集和验证集
    dataset = TensorDataset(all_input_ids, labels)
    train_size = int(0.90 * len(dataset))
    val_size = len(dataset) - train_size
    train_dataset, val_dataset = random_split(dataset, [train_size, val_size])
    
    # Create train and validation dataloaders
    train_dataloader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
    val_dataloader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    加载bert模型

    # Load the pretrained BERT model
    model = BertForSequenceClassification.from_pretrained('bert-base-uncased', num_labels=2, output_attentions=False, output_hidden_states=False)
    model.cuda()
    
    # create optimizer and learning rate schedule
    optimizer = AdamW(model.parameters(), lr=2e-5)
    # len(train_dataloader),数据被分为N个批次,每个批次的数据长度是32
    # total_steps 
    total_steps = len(train_dataloader) * epochs
    # get_linear_schedule_with_warmup: 创建学习率调度器函数,
    # get_linear_schedule_with_warmup函数在训练初期使用一种称为"warm-up"的策略,即在初始几个epoch中逐步增加学习率,以帮助模型更快地收敛到一个相对合适的参数范围。然后,在warm-up之后,学习率线性地减少或保持恒定。
    scheduler = get_linear_schedule_with_warmup(optimizer, num_warmup_steps=0, num_training_steps=total_steps)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    准确率计算
    在这里插入图片描述

    from sklearn.metrics import f1_score, accuracy_score
    
    def flat_accuracy(preds, labels):
        
        """A function for calculating accuracy scores"""
        
        pred_flat = np.argmax(preds, axis=1).flatten()
        # 将真实标签数组labels进行展平,得到一维的真实标签数组。
        labels_flat = labels.flatten()
        # 计算展平后的真实标签数组与预测结果数组之间的准确率
        return accuracy_score(labels_flat, pred_flat)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    训练和验证

    for epoch in range(epochs):
    	# 将模型设置为训练模式
        model.train()
        total_loss, total_val_loss = 0, 0
        total_eval_accuracy = 0
        # step:步数
        # batch : 每一个批次的数据
        for step, batch in enumerate(train_dataloader):
            model.zero_grad() 
            loss, logits = model(batch[0].to(device), token_type_ids=None, attention_mask=(batch[0]>0).to(device), labels=batch[1].to(device))
            total_loss += loss.item()
            loss.backward()
            torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
            optimizer.step() 
            scheduler.step()
        # model.eval()是PyTorch中用于将模型设置为评估模式(evaluation mode)的方法。在深度学习中,模型通常有两种模式:训练模式(training mode)和评估模式(evaluation mode)。
        model.eval()
        for i, batch in enumerate(val_dataloader):
            with torch.no_grad():
                loss, logits = model(batch[0].to(device), token_type_ids=None, attention_mask=(batch[0]>0).to(device), labels=batch[1].to(device))
                    
                total_val_loss += loss.item()
                
                logits = logits.detach().cpu().numpy()
                label_ids = batch[1].to('cpu').numpy()
                total_eval_accuracy += flat_accuracy(logits, label_ids)
        
        avg_train_loss = total_loss / len(train_dataloader)
        avg_val_loss = total_val_loss / len(val_dataloader)
        avg_val_accuracy = total_eval_accuracy / len(val_dataloader)
        
        print(f'Train loss     : {avg_train_loss}')
        print(f'Validation loss: {avg_val_loss}')
        print(f'Accuracy: {avg_val_accuracy:.2f}')
        print('\n')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    Train loss : 0.441781023875452
    Validation loss: 0.34831519580135745
    Accuracy: 0.86
    Train loss : 0.3275374324204257
    Validation loss: 0.3286557973672946
    Accuracy: 0.88
    Train loss : 0.2503694619696874
    Validation loss: 0.355623895690466
    Accuracy: 0.86
    Train loss : 0.19663514375973207
    Validation loss: 0.3806843503067891
    Accuracy: 0.86

    预测

    # Create the test data loader
    text_values = df_test['final_text'].values
    # 将数据转化为输入模型需要的数据类型
    all_input_ids = encode_fn(text_values)
    pred_data = TensorDataset(all_input_ids)
    # 
    pred_dataloader = DataLoader(pred_data, batch_size=batch_size, shuffle=False)
    # 设置模型为评估模式。
    model.eval()
    preds = []
    for i, (batch,) in enumerate(pred_dataloader):
        with torch.no_grad():
            outputs = model(batch.to(device), token_type_ids=None, attention_mask=(batch>0).to(device))
            logits = outputs[0]
            logits = logits.detach().cpu().numpy()
            preds.append(logits)
    
    final_preds = np.concatenate(preds, axis=0)
    final_preds = np.argmax(final_preds, axis=1)
    
    # Create submission file
    submission = pd.DataFrame()
    submission['id'] = df_test['id']
    submission['target'] = final_preds
    submission.to_csv('submission.csv', index=False)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    参考
    NLP with Disaster Tweets EDA

    竞赛网址

    总结

    🎵~ 出卖我的爱,你背了良心债,最后知道真相的我眼泪掉下来。

  • 相关阅读:
    Part2_扩展MATSIM_Subpart4_除个人车外的其他模式_第16章 用Matsim建模公共交通
    使用C#/.NET解析Wiki百科数据实现获取历史上的今天
    聊聊流式数据湖Paimon(四)
    自定义协议、序列化与反序列化
    沉浸其境,共赴云栖数智硬核美学
    十年架构五年生活-09 五年之约如期而至
    Nginx配置整合:基本概念、命令、反向代理、负载均衡、动静分离、高可用
    java面试之恒生电子①
    【Pytorch深度学习实战】(12)神经风格迁移(Neural Style Transfer)
    (动手学习深度学习)第13章 计算机视觉---微调
  • 原文地址:https://blog.csdn.net/weixin_42475060/article/details/131814166