• 【NLP】使用 PyTorch 通过 Hugging Face 使用 BERT 和 Transformers 进行情感分析


     🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎

    📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃

    🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝​

    📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】  深度学习【DL】

     🖍foreword

    ✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。

    如果你对这个系列感兴趣的话,可以关注订阅哟👋

    文章目录

    什么是BERT?

    掩码语言建模(Masked LM)

    下一句预测(NSP)

    这东西在实践中有用吗?

    设置

    数据探索

    数据预处理

    Special Tokens

    选择序列长度

    使用 BERT 和Hugging Face进行情感分类

    训练

    评估

    预测原始文本

    概括


    在本教程中,您将学习如何微调 BERT 以进行情感分析。您将进行所需的文本预处理(特殊标记、填充和注意掩码),并使用 Hugging Face 令人惊叹的 Transformers 库构建情感分类器!

    您将学习如何:

    • 直观了解什么是BERT
    • 为 BERT 预处理文本数据并构建 PyTorch 数据集(标记化、注意掩码和填充)
    • 通过 Hugging Face 使用 Transformers 库使用迁移学习构建情感分类器
    • 根据测试数据评估模型
    • 预测原始文本的情绪

    什么是BERT?

    BERT(在本文中介绍)代表来自 Transformers 的双向编码器表示。如果您不知道其中大部分是什么意思 - 您来对地方了!让我们解开主要思想:

    • 双向 - 要理解您正在查看的文本,您必须向后看(在前面的单词)和向前看(在下一个单词)
    • Transformers - The Attention Is All You Need论文介绍了 Transformer 模型。Transformer 一次读取整个令牌序列。从某种意义上说,该模型是非定向的,而 LSTM 是按顺序读取的(从左到右或从右到左)。注意机制允许学习单词之间的上下文关系(例如his,在一个句子中指的是吉姆)。
    • (预训练的)上下文词嵌入——ELMO 论文介绍了一种根据词义/上下文对词进行编码的方法。指甲有多重含义——手指甲和金属钉。

    BERT 通过屏蔽 15% 的标记进行训练,目的是猜测它们。另一个目标是预测下一句话。让我们看一下这些任务的示例:

    掩码语言建模(Masked LM)

    此任务的目的是猜测掩码标记。让我们看一个例子,尽量不要让它变得比它必须的更难:

    That’s [mask] she [mask] -> That’s what she said

    下一句预测(NSP)

    给定一对两个句子,任务是判断第二个是否跟在第一个之后(二元分类)。让我们继续这个例子:

    Input = [CLS] That’s [mask] she [mask]. [SEP] Hahaha, nice! [SEP]

    Label = IsNext

    Input = [CLS] That’s [mask] she [mask]. [SEP] Dwight, you ignorant [mask]! [SEP]

    Label = NotNext

    训练语料库由两个条目组成:多伦多图书语料库(800M 词)和英语维基百科(2,500M 词)。原始的 Transformer 有一个编码器(用于读取输入)和一个解码器(进行预测),而 BERT 只使用解码器。

    BERT 只是一组预训练的 Transformer 编码器。多少个编码器?我们有两个版本——12(BERT base)和 24(BERT Large)。

    这东西在实践中有用吗?

    BERT 论文与源代码和预训练模型一起发布。

    最好的部分是,您可以使用 BERT 进行迁移学习(得益于 OpenAI Transformer 的想法)以完成许多 NLP 任务——分类、问答、实体识别等。您可以使用少量数据进行训练并获得出色的性能!

    设置

    我们需要Hugging Face的 Transformers 库

    !pip install -qq transformers
    1. %reload_ext watermark
    2. %watermark -v -p numpy,pandas,torch,transformers
    3. CPython 3.6.9
    4. IPython 5.5.0
    5. numpy 1.18.2
    6. pandas 1.0.3
    7. torch 1.4.0
    8. transformers 2.8.0
    1. import transformers
    2. from transformers import BertModel, BertTokenizer, AdamW, get_linear_schedule_with_warmup
    3. import torch
    4. import numpy as np
    5. import pandas as pd
    6. import seaborn as sns
    7. from pylab import rcParams
    8. import matplotlib.pyplot as plt
    9. from matplotlib import rc
    10. from sklearn.model_selection import train_test_split
    11. from sklearn.metrics import confusion_matrix, classification_report
    12. from collections import defaultdict
    13. from textwrap import wrap
    14. from torch import nn, optim
    15. from torch.utils.data import Dataset, DataLoader
    16. %matplotlib inline
    17. %config InlineBackend.figure_format='retina'
    18. sns.set(style='whitegrid', palette='muted', font_scale=1.2)
    19. HAPPY_COLORS_PALETTE = ["#01BEFE", "#FFDD00", "#FF7D00", "#FF006D", "#ADFF02", "#8F00FF"]
    20. sns.set_palette(sns.color_palette(HAPPY_COLORS_PALETTE))
    21. rcParams['figure.figsize'] = 12, 8
    22. RANDOM_SEED = 42
    23. np.random.seed(RANDOM_SEED)
    24. torch.manual_seed(RANDOM_SEED)
    25. device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

    数据探索

    我们将加载我们在上一部分中汇总的 Google Play 应用评论数据集:

    1. !gdown --id 1S6qMioqPJjyBLpLVz4gmRTnJHnjitnuV
    2. !gdown --id 1zdmewp7ayS4js4VtrJEHzAheSW-5NBZv
    1. df = pd.read_csv("reviews.csv")
    2. df.head()
    userNameuserImagecontentscorethumbsUpCountreviewCreatedVersionatreplyContentrepliedAtsortOrderappId
    0Andrew Thomashttps://lh3.googleusercontent.com/a-/AOh14GiHd...Update: After getting a response from the deve...1214.17.0.32020-04-05 22:25:57According to our TOS, and the term you have ag...2020-04-05 15:10:24most_relevantcom.anydo
    1Craig Haineshttps://lh3.googleusercontent.com/-hoe0kwSJgPQ...Used it for a fair amount of time without any ...1114.17.0.32020-04-04 13:40:01It sounds like you logged in with a different ...2020-04-05 15:11:35most_relevantcom.anydo
    2steven adkinshttps://lh3.googleusercontent.com/a-/AOh14GiXw...Your app sucks now!!!!! Used to be good but no...1174.17.0.32020-04-01 16:18:13This sounds odd! We are not aware of any issue...2020-04-02 16:05:56most_relevantcom.anydo
    3Lars Panzerbjørnhttps://lh3.googleusercontent.com/a-/AOh14Gg-h...It seems OK, but very basic. Recurring tasks n...11924.17.0.22020-03-12 08:17:34We do offer this option as part of the Advance...2020-03-15 06:20:13most_relevantcom.anydo
    4Scott Prewitthttps://lh3.googleusercontent.com/-K-X1-YsVd6U...Absolutely worthless. This app runs a prohibit...1424.17.0.22020-03-14 17:41:01We're sorry you feel this way! 90% of the app ...2020-03-15 23:45:51most_relevantcom.anydo
    df.shape

     (15746, 11)

    我们有大约 16k 个示例。让我们检查缺失值:

    df.info()
    1. <class 'pandas.core.frame.DataFrame'>
    2. RangeIndex: 15746 entries, 0 to 15745
    3. Data columns (total 11 columns):
    4. # Column Non-Null Count Dtype
    5. --- ------ -------------- -----
    6. 0 userName 15746 non-null object
    7. 1 userImage 15746 non-null object
    8. 2 content 15746 non-null object
    9. 3 score 15746 non-null int64
    10. 4 thumbsUpCount 15746 non-null int64
    11. 5 reviewCreatedVersion 13533 non-null object
    12. 6 at 15746 non-null object
    13. 7 replyContent 7367 non-null object
    14. 8 repliedAt 7367 non-null object
    15. 9 sortOrder 15746 non-null object
    16. 10 appId 15746 non-null object
    17. dtypes: int64(2), object(9)
    18. memory usage: 1.3+ MB

    太棒了,评分和评论文本中没有缺失值!我们有阶级失衡吗?

    1. sns.countplot(df.score)
    2. plt.xlabel('review score');

     这是非常不平衡的,但没关系。我们要将数据集转换为负面、中性和正面情绪:

    1. def to_sentiment(rating):
    2. rating = int(rating)
    3. if rating <= 2:
    4. return 0
    5. elif rating == 3:
    6. return 1
    7. else:
    8. return 2
    9. df['sentiment'] = df.score.apply(to_sentiment)
    10. class_names = ['negative', 'neutral', 'positive']
    11. ax = sns.countplot(df.sentiment)
    12. plt.xlabel('review sentiment')
    13. ax.set_xticklabels(class_names);

       

    平衡(大部分)恢复了。

    数据预处理

    您可能已经知道机器学习模型不适用于原始文本。您需要将文本转换为数字(某种)。BERT 需要更多的关注(好的,对吧?)。以下是要求:

    • 添加特殊标记来分隔句子并进行分类
    • 传递恒定长度的序列(引入填充)
    • 创建 0s(pad token)和 1s(real token)的数组,称为注意力掩码

    Transformers 库提供(您已经猜到了)各种各样的 Transformer 模型(包括 BERT)。它适用于 TensorFlow 和 PyTorch!它还包括为我们完成繁重工作的预构建分词器!

    PRE_TRAINED_MODEL_NAME= 'bert-base-cased'

    您可以使用 BERT 和分词器的大小写和非大小写版本。我已经尝试过两者。外壳版本效果更好。直觉上,这是有道理的,因为“BAD”可能比“bad”传达更多情绪。

    让我们加载一个预训练的BertTokenizer

    tokenizer = BertTokenizer.from_pretrained(PRE_TRAINED_MODEL_NAME)

    我们将使用此文本来了解标记化过程:

    sample_txt = 'When was I last outside? I am stuck at home for 2 weeks.'

    一些基本操作可以将文本转换为标记,并将标记转换为唯一的整数 (ids):

    1. tokens = tokenizer.tokenize(sample_txt)
    2. token_ids = tokenizer.convert_tokens_to_ids(tokens)
    3. print(f' Sentence: {sample_txt}')
    4. print(f' Tokens: {tokens}')
    5. print(f'Token IDs: {token_ids}')
    Sentence: When was I last outside? I am stuck at home for 2 weeks.
    Tokens: ['When', 'was', 'I', 'last', 'outside', '?', 'I', 'am', 'stuck', 'at', 'home', 'for', '2', 'weeks', '.']
    Token IDs: [1332, 1108, 146, 1314, 1796, 136, 146, 1821, 5342, 1120, 1313, 1111, 123, 2277, 119]
    

    Special Tokens

    [SEP]- 句子结束标记

    tokenizer.sep_token, tokenizer.sep_token_id

    ('[SEP]', 102) 

    [CLS]- 我们必须将此标记添加到每个句子的开头,以便 BERT 知道我们在进行分类

    tokenizer.cls_token, tokenizer.cls_token_id

    ('[CLS]', 101)

    还有一个用于填充的特殊标记:

    tokenizer.pad_token, tokenizer.pad_token_id

    ('[PAD]', 0)

    BERT 理解训练集中的标记。其他一切都可以使用[UNK](未​​知)令牌进行编码:

    tokenizer.unk_token, tokenizer.unk_token_id

    ('[UNK]', 100)

    所有这些工作都可以使用以下encode_plus()方法完成:

    1. encoding = tokenizer.encode_plus(
    2. sample_txt,
    3. max_length=32,
    4. add_special_tokens=True, # Add '[CLS]' and '[SEP]'
    5. return_token_type_ids=False,
    6. pad_to_max_length=True,
    7. return_attention_mask=True,
    8. return_tensors='pt', # Return PyTorch tensors
    9. )
    10. encoding.keys()

    dict_keys(['input_ids', 'attention_mask'])

    令牌 id 现在存储在张量中并填充到 32 的长度:

    1. print(len(encoding['input_ids'][0]))
    2. encoding['input_ids'][0]
    32
    tensor([ 101, 1332, 1108, 146, 1314, 1796, 136, 146, 1821, 5342, 1120, 1313,
             1111, 123, 2277, 119, 102, 0, 0, 0, 0, 0, 0, 0,
             0, 0, 0, 0, 0, 0, 0, 0])

    注意掩码具有相同的长度:

    1. print(len(encoding['attention_mask'][0]))
    2. encoding['attention_mask']
    32
    tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0]])

    我们可以反转标记化以查看特殊标记:

    tokenizer.convert_ids_to_tokens(encoding['input_ids'][0])
    1. ['[CLS]',
    2. 'When',
    3. 'was',
    4. 'I',
    5. 'last',
    6. 'outside',
    7. '?',
    8. 'I',
    9. 'am',
    10. 'stuck',
    11. 'at',
    12. 'home',
    13. 'for',
    14. '2',
    15. 'weeks',
    16. '.',
    17. '[SEP]',
    18. '[PAD]',
    19. '[PAD]',
    20. '[PAD]',
    21. '[PAD]',
    22. '[PAD]',
    23. '[PAD]',
    24. '[PAD]',
    25. '[PAD]',
    26. '[PAD]',
    27. '[PAD]',
    28. '[PAD]',
    29. '[PAD]',
    30. '[PAD]',
    31. '[PAD]',
    32. '[PAD]']

    选择序列长度

    BERT 适用于固定长度的序列。我们将使用一个简单的策略来选择最大长度。让我们存储每个评论的令牌长度:

    1. token_lens = []
    2. for txt in df.content:
    3. tokens = tokenizer.encode(txt, max_length=512)
    4. token_lens.append(len(tokens))

    并绘制分布:

    1. sns.distplot(token_lens)
    2. plt.xlim([0, 256]);
    3. plt.xlabel('Token count');

    大多数评论似乎包含少于 128 个标记,但为了安全起见,我们选择最大长度为 160。

    MAX_LEN = 160

    我们拥有创建 PyTorch 数据集所需的所有构建块。我们开始做吧:

    1. class GPReviewDataset(Dataset):
    2. def __init__(self, reviews, targets, tokenizer, max_len):
    3. self.reviews = reviews
    4. self.targets = targets
    5. self.tokenizer = tokenizer
    6. self.max_len = max_len
    7. def __len__(self):
    8. return len(self.reviews)
    9. def __getitem__(self, item):
    10. review = str(self.reviews[item])
    11. target = self.targets[item]
    12. encoding = self.tokenizer.encode_plus(
    13. review,
    14. add_special_tokens=True,
    15. max_length=self.max_len,
    16. return_token_type_ids=False,
    17. pad_to_max_length=True,
    18. return_attention_mask=True,
    19. return_tensors='pt',
    20. )
    21. return {
    22. 'review_text': review,
    23. 'input_ids': encoding['input_ids'].flatten(),
    24. 'attention_mask': encoding['attention_mask'].flatten(),
    25. 'targets': torch.tensor(target, dtype=torch.long)
    26. }

    分词器为我们做了大部分繁重的工作。我们还会返回评论文本,因此可以更轻松地评估我们模型的预测。让我们拆分数据: 

    1. df_train, df_test = train_test_split(
    2. df,
    3. test_size=0.1,
    4. random_state=RANDOM_SEED
    5. )
    6. df_val, df_test = train_test_split(
    7. df_test,
    8. test_size=0.5,
    9. random_state=RANDOM_SEED
    10. )
    11. df_train.shape, df_val.shape, df_test.shape

    ((14171, 12), (787, 12), (788, 12)) 

    我们还需要创建几个数据加载器。这是一个辅助函数:

    1. def create_data_loader(df, tokenizer, max_len, batch_size):
    2. ds = GPReviewDataset(
    3. reviews=df.content.to_numpy(),
    4. targets=df.sentiment.to_numpy(),
    5. tokenizer=tokenizer,
    6. max_len=max_len
    7. )
    8. return DataLoader(
    9. ds,
    10. batch_size=batch_size,
    11. num_workers=4
    12. )
    13. BATCH_SIZE = 16
    14. train_data_loader = create_data_loader(df_train, tokenizer, MAX_LEN, BATCH_SIZE)
    15. val_data_loader = create_data_loader(df_val, tokenizer, MAX_LEN, BATCH_SIZE)
    16. test_data_loader = create_data_loader(df_test, tokenizer, MAX_LEN, BATCH_SIZE)

    让我们看一下来自训练数据加载器的示例批次: 

    1. data = next(iter(train_data_loader))
    2. data.keys()

    dict_keys(['review_text', 'input_ids', 'attention_mask', 'targets']) 

    1. print(data['input_ids'].shape)
    2. print(data['attention_mask'].shape)
    3. print(data['targets'].shape)
    torch.Size([16, 160])
    torch.Size([16, 160])
    torch.Size([16])

    使用 BERT 和Hugging Face进行情感分类

    有很多助手可以通过 Transformers 库轻松使用 BERT。根据您可能想要使用BertForSequenceClassificationBertForQuestionAnswering或其他东西的任务。

    但谁在乎,对吧?我们是铁杆!我们将使用基本的BertModel并在其之上构建我们的情感分类器。让我们加载模型:

    bert_model = BertModel.from_pretrained(PRE_TRAINED_MODEL_NAME)

    并尝试在我们的示例文本的编码中使用它:

    1. last_hidden_state, pooled_output = bert_model(
    2. input_ids=encoding['input_ids'],
    3. attention_mask=encoding['attention_mask']
    4. )

    是模型最后一层的last_hidden_state一系列隐藏状态。获取pooled_output是通过在 上应用BertPooler来完成的last_hidden_state: 

    last_hidden_state.shape

    torch.Size([1, 32, 768]) 

    我们有 32 个标记(示例序列的长度)中每一个的隐藏状态。但为什么是 768?这是前馈网络中隐藏单元的数量。我们可以通过检查配置来验证:

    bert_model.config.hidden_size

    768 

    pooled_output根据 BERT,您可以将其视为内容摘要。尽管如此,您可能会尝试做得更好。让我们看看输出的形状:

    pooled_output.shape

    torch.Size([1, 768]) 

    我们可以使用所有这些知识来创建一个使用 BERT 模型的分类器:

    1. class SentimentClassifier(nn.Module):
    2. def __init__(self, n_classes):
    3. super(SentimentClassifier, self).__init__()
    4. self.bert = BertModel.from_pretrained(PRE_TRAINED_MODEL_NAME)
    5. self.drop = nn.Dropout(p=0.3)
    6. self.out = nn.Linear(self.bert.config.hidden_size, n_classes)
    7. def forward(self, input_ids, attention_mask):
    8. _, pooled_output = self.bert(
    9. input_ids=input_ids,
    10. attention_mask=attention_mask
    11. )
    12. output = self.drop(pooled_output)
    13. return self.out(output

    我们的分类器将大部分繁重的工作委托给了 BertModel。我们使用 dropout 层进行正则化,使用全连接层进行输出。请注意,我们要返回最后一层的原始输出,因为 PyTorch 中的交叉熵损失函数需要它才能工作。

    这应该像任何其他 PyTorch 模型一样工作。让我们创建一个实例并将其移动到 GPU

    1. model = SentimentClassifier(len(class_names))
    2. model = model.to(device)

     我们会将训练数据的示例批次移动到 GPU:

    1. input_ids = data['input_ids'].to(device)
    2. attention_mask = data['attention_mask'].to(device)
    3. print(input_ids.shape) # batch size x seq length
    4. print(attention_mask.shape) # batch size x seq length
    torch.Size([16, 160])
    torch.Size([16, 160])

     为了从我们训练的模型中获得预测概率,我们将 softmax 函数应用于输出:

    F.softmax(model(input_ids, attention_mask), dim=1)
    1. tensor([[0.5879, 0.0842, 0.3279],
    2. [0.4308, 0.1888, 0.3804],
    3. [0.4871, 0.1766, 0.3363],
    4. [0.3364, 0.0778, 0.5858],
    5. [0.4025, 0.1040, 0.4935],
    6. [0.3599, 0.1026, 0.5374],
    7. [0.5054, 0.1552, 0.3394],
    8. [0.5962, 0.1464, 0.2574],
    9. [0.3274, 0.1967, 0.4759],
    10. [0.3026, 0.1118, 0.5856],
    11. [0.4103, 0.1571, 0.4326],
    12. [0.4879, 0.2121, 0.3000],
    13. [0.3811, 0.1477, 0.4712],
    14. [0.3354, 0.1354, 0.5292],
    15. [0.3999, 0.2822, 0.3179],
    16. [0.5075, 0.1684, 0.3242]], device='cuda:0', grad_fn=<SoftmaxBackward>)

    训练

    为了重现 BERT 论文中的训练过程,我们将使用Hugging Face 提供的AdamW优化器。它纠正了权重衰减,因此它与原始论文相似。我们还将使用没有预热步骤的线性调度程序:

    1. EPOCHS = 10
    2. optimizer = AdamW(model.parameters(), lr=2e-5, correct_bias=False)
    3. total_steps = len(train_data_loader) * EPOCHS
    4. scheduler = get_linear_schedule_with_warmup(
    5. optimizer,
    6. num_warmup_steps=0,
    7. num_training_steps=total_steps
    8. )
    9. loss_fn = nn.CrossEntropyLoss().to(device)

    我们如何提出所有超参数?BERT 作者有一些微调建议:

    • 批量大小:16、32
    • 学习率(Adam):5e-5、3e-5、2e-5
    • epochs数:2、3、4

    我们将忽略 epochs 推荐的数量,但坚持使用其余的。请注意,增加批量大小会显着减少训练时间,但会降低准确性。

    让我们继续编写一个辅助函数来训练我们的模型一个时期:

    1. def train_epoch(
    2. model,
    3. data_loader,
    4. loss_fn,
    5. optimizer,
    6. device,
    7. scheduler,
    8. n_examples
    9. ):
    10. model = model.train()
    11. losses = []
    12. correct_predictions = 0
    13. for d in data_loader:
    14. input_ids = d["input_ids"].to(device)
    15. attention_mask = d["attention_mask"].to(device)
    16. targets = d["targets"].to(device)
    17. outputs = model(
    18. input_ids=input_ids,
    19. attention_mask=attention_mask
    20. )
    21. _, preds = torch.max(outputs, dim=1)
    22. loss = loss_fn(outputs, targets)
    23. correct_predictions += torch.sum(preds == targets)
    24. losses.append(loss.item())
    25. loss.backward()
    26. nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
    27. optimizer.step()
    28. scheduler.step()
    29. optimizer.zero_grad()
    30. return correct_predictions.double() / n_examples, np.mean(losses)

    训练模型应该看起来很熟悉,除了两件事。每次将批次提供给模型时都会调用调度程序。我们通过使用clip grad_norm裁剪模型的梯度来避免爆炸梯度。

    让我们编写另一个帮助我们在给定数据加载器上评估模型的方法:

    1. def eval_model(model, data_loader, loss_fn, device, n_examples):
    2. model = model.eval()
    3. losses = []
    4. correct_predictions = 0
    5. with torch.no_grad():
    6. for d in data_loader:
    7. input_ids = d["input_ids"].to(device)
    8. attention_mask = d["attention_mask"].to(device)
    9. targets = d["targets"].to(device)
    10. outputs = model(
    11. input_ids=input_ids,
    12. attention_mask=attention_mask
    13. )
    14. _, preds = torch.max(outputs, dim=1)
    15. loss = loss_fn(outputs, targets)
    16. correct_predictions += torch.sum(preds == targets)
    17. losses.append(loss.item())
    18. return correct_predictions.double() / n_examples, np.mean(losses)

     使用这两个,我们可以编写我们的训练循环。我们还将存储训练历史:

    1. %%time
    2. history = defaultdict(list)
    3. best_accuracy = 0
    4. for epoch in range(EPOCHS):
    5. print(f'Epoch {epoch + 1}/{EPOCHS}')
    6. print('-' * 10)
    7. train_acc, train_loss = train_epoch(
    8. model,
    9. train_data_loader,
    10. loss_fn,
    11. optimizer,
    12. device,
    13. scheduler,
    14. len(df_train)
    15. )
    16. print(f'Train loss {train_loss} accuracy {train_acc}')
    17. val_acc, val_loss = eval_model(
    18. model,
    19. val_data_loader,
    20. loss_fn,
    21. device,
    22. len(df_val)
    23. )
    24. print(f'Val loss {val_loss} accuracy {val_acc}')
    25. print()
    26. history['train_acc'].append(train_acc)
    27. history['train_loss'].append(train_loss)
    28. history['val_acc'].append(val_acc)
    29. history['val_loss'].append(val_loss)
    30. if val_acc > best_accuracy:
    31. torch.save(model.state_dict(), 'best_model_state.bin')
    32. best_accuracy = val_acc
    1. Epoch 1/10
    2. ----------
    3. Train loss 0.7330631300571541 accuracy 0.6653729447463129
    4. Val loss 0.5767546480894089 accuracy 0.7776365946632783
    5. Epoch 2/10
    6. ----------
    7. Train loss 0.4158683338330777 accuracy 0.8420012701997036
    8. Val loss 0.5365073362737894 accuracy 0.832274459974587
    9. Epoch 3/10
    10. ----------
    11. Train loss 0.24015077009679367 accuracy 0.922023851527768
    12. Val loss 0.5074492372572422 accuracy 0.8716645489199493
    13. Epoch 4/10
    14. ----------
    15. Train loss 0.16012676668187295 accuracy 0.9546962105708843
    16. Val loss 0.6009970247745514 accuracy 0.8703939008894537
    17. Epoch 5/10
    18. ----------
    19. Train loss 0.11209654617575301 accuracy 0.9675393409074872
    20. Val loss 0.7367783848941326 accuracy 0.8742058449809403
    21. Epoch 6/10
    22. ----------
    23. Train loss 0.08572274737026433 accuracy 0.9764307388328276
    24. Val loss 0.7251267762482166 accuracy 0.8843710292249047
    25. Epoch 7/10
    26. ----------
    27. Train loss 0.06132202987342602 accuracy 0.9833462705525369
    28. Val loss 0.7083295831084251 accuracy 0.889453621346887
    29. Epoch 8/10
    30. ----------
    31. Train loss 0.050604159273123096 accuracy 0.9849693035071626
    32. Val loss 0.753860274553299 accuracy 0.8907242693773825
    33. Epoch 9/10
    34. ----------
    35. Train loss 0.04373276197092931 accuracy 0.9862395032107826
    36. Val loss 0.7506809896230697 accuracy 0.8919949174078781
    37. Epoch 10/10
    38. ----------
    39. Train loss 0.03768671146314381 accuracy 0.9880036694658105
    40. Val loss 0.7431786182522774 accuracy 0.8932655654383737
    41. CPU times: user 29min 54s, sys: 13min 28s, total: 43min 23s
    42. Wall time: 43min 43s

    请注意,我们正在存储最佳模型的状态,以最高验证准确度表示。

    哇,这花了一些时间!我们可以看看训练与验证的准确性:

    1. plt.plot(history['train_acc'], label='train accuracy')
    2. plt.plot(history['val_acc'], label='validation accuracy')
    3. plt.title('Training history')
    4. plt.ylabel('Accuracy')
    5. plt.xlabel('Epoch')
    6. plt.legend()
    7. plt.ylim([0, 1]);

     

     训练准确率在 10 个 epoch 左右后开始接近 100%。您可能会尝试对参数进行更多微调,但这对我们来说已经足够了。

    不想等?取消注释下一个单元格以下载我的预训练模型:

    1. # !gdown --id 1V8itWtowCYnb2Bc9KlK9SxGff9WwmogA
    2. # model = SentimentClassifier(len(class_names))
    3. # model.load_state_dict(torch.load('best_model_state.bin'))
    4. # model = model.to(device)

    评估

    那么我们的模型在预测情绪方面有多好?让我们从计算测试数据的准确性开始:

    1. test_acc, _ = eval_model(
    2. model,
    3. test_data_loader,
    4. loss_fn,
    5. device,
    6. len(df_test)
    7. )
    8. test_acc.item()

    0.883248730964467 

    测试集上的准确率大约低 1%。我们的模型似乎概括得很好。

    我们将定义一个辅助函数来从我们的模型中获取预测:

    1. def get_predictions(model, data_loader):
    2. model = model.eval()
    3. review_texts = []
    4. predictions = []
    5. prediction_probs = []
    6. real_values = []
    7. with torch.no_grad():
    8. for d in data_loader:
    9. texts = d["review_text"]
    10. input_ids = d["input_ids"].to(device)
    11. attention_mask = d["attention_mask"].to(device)
    12. targets = d["targets"].to(device)
    13. outputs = model(
    14. input_ids=input_ids,
    15. attention_mask=attention_mask
    16. )
    17. _, preds = torch.max(outputs, dim=1)
    18. review_texts.extend(texts)
    19. predictions.extend(preds)
    20. prediction_probs.extend(outputs)
    21. real_values.extend(targets)
    22. predictions = torch.stack(predictions).cpu()
    23. prediction_probs = torch.stack(prediction_probs).cpu()
    24. real_values = torch.stack(real_values).cpu()
    25. return review_texts, predictions, prediction_probs, real_values

    这类似于评估函数,除了我们存储评论文本和预测概率: 

    1. y_review_texts, y_pred, y_pred_probs, y_test = get_predictions(
    2. model,
    3. test_data_loader
    4. )

    我们来看看分类报告 

    print(classification_report(y_test, y_pred, target_names=class_names))
    1. precision recall f1-score support
    2. negative 0.89 0.87 0.88 245
    3. neutral 0.83 0.85 0.84 254
    4. positive 0.92 0.93 0.92 289
    5. accuracy 0.88 788
    6. macro avg 0.88 0.88 0.88 788
    7. weighted avg 0.88 0.88 0.88 788

     看起来很难对中性(3 星)评论进行分类。我可以根据经验告诉你,看了很多评论,这些评论很难归类。

    我们将继续混淆矩阵:

    1. def show_confusion_matrix(confusion_matrix):
    2. hmap = sns.heatmap(confusion_matrix, annot=True, fmt="d", cmap="Blues")
    3. hmap.yaxis.set_ticklabels(hmap.yaxis.get_ticklabels(), rotation=0, ha='right')
    4. hmap.xaxis.set_ticklabels(hmap.xaxis.get_ticklabels(), rotation=30, ha='right')
    5. plt.ylabel('True sentiment')
    6. plt.xlabel('Predicted sentiment');
    7. cm = confusion_matrix(y_test, y_pred)
    8. df_cm = pd.DataFrame(cm, index=class_names, columns=class_names)
    9. show_confusion_matrix(df_cm)
     
    

    1      这证实了我们的模型难以对中性评论进行分类。它以大致相同的频率将那些误认为是负面的和正面的。

    这是对我们模型性能的一个很好的概述。但是让我们看一下测试数据中的一个例子:

    1. idx = 2
    2. review_text = y_review_texts[idx]
    3. true_sentiment = y_test[idx]
    4. pred_df = pd.DataFrame({
    5. 'class_names': class_names,
    6. 'values': y_pred_probs[idx]
    7. })
    8. print("\n".join(wrap(review_text)))
    9. print()
    10. print(f'True sentiment: {class_names[true_sentiment]}')
    1. I used to use Habitica, and I must say this is a great step up. I'd
    2. like to see more social features, such as sharing tasks - only one
    3. person has to perform said task for it to be checked off, but only
    4. giving that person the experience and gold. Otherwise, the price for
    5. subscription is too steep, thus resulting in a sub-perfect score. I
    6. could easily justify $0.99/month or eternal subscription for $15. If
    7. that price could be met, as well as fine tuning, this would be easily
    8. worth 5 stars.
    9. True sentiment: neutral

     现在我们可以查看模型中每种情绪的置信度:

    1. sns.barplot(x='values', y='class_names', data=pred_df, orient='h')
    2. plt.ylabel('sentiment')
    3. plt.xlabel('probability')
    4. plt.xlim([0, 1]);

    预测原始文本

    让我们使用我们的模型来预测一些原始文本的情绪:

    review_text = "I love completing my todos! Best app ever!!!"

    我们必须使用分词器对文本进行编码:

    1. encoded_review = tokenizer.encode_plus(
    2. review_text,
    3. max_length=MAX_LEN,
    4. add_special_tokens=True,
    5. return_token_type_ids=False,
    6. pad_to_max_length=True,
    7. return_attention_mask=True,
    8. return_tensors='pt',
    9. )

     让我们从我们的模型中得到预测:

    1. input_ids = encoded_review['input_ids'].to(device)
    2. attention_mask = encoded_review['attention_mask'].to(device)
    3. output = model(input_ids, attention_mask)
    4. _, prediction = torch.max(output, dim=1)
    5. print(f'Review text: {review_text}')
    6. print(f'Sentiment : {class_names[prediction]}')
    Review text: I love completing my todos! Best app ever!!!
    Sentiment : positive

    概括

    不错的工作!您学习了如何使用 BERT 进行情感分析。您使用 Hugging Face 库构建了一个自定义分类器,并在我们的应用评论数据集上对其进行了训练!

    您学会了如何:

    • 直观了解什么是BERT
    • 为 BERT 预处理文本数据并构建 PyTorch 数据集(标记化、注意掩码和填充)
    • 通过 Hugging Face 使用 Transformers 库使用迁移学习构建情感分类器
    • 根据测试数据评估模型
    • 预测原始文本的情绪
  • 相关阅读:
    vivado产生报告阅读分析5-时序报告1
    C++基础语法——智能指针
    stm32f103步进电机S曲线加减速计算
    IDEA XML文件里写SQL比较大小条件
    基于PHP的编程类学习网站设计与实现
    易语言软件定制开发爬虫模拟协议填写自动化办公软件开发多人团队
    ChatGPT魔法,定制个性化提示词!
    基于JAVA的电子书城系统(Web)
    kubeadm (etcd)
    《统计学习方法》 第十一章 条件随机场(原理+代码)
  • 原文地址:https://blog.csdn.net/sikh_0529/article/details/127950840