在 Colab 中打开  在 GitHub 上查看 Notebook

💭 增强情感分析:基于跨度的极性方法与 Setfit#

在本教程中,我们将使用 Setfit ABSA 库微调用于二元方面级情感分析 (ABSA) 的预训练模型。然后,我们将使用 Argilla 进行一些预测并评估结果。

我们将遵循以下步骤

  • 准备具有所需格式的数据集。

  • 使用 Setfit ABSA 进行微调。

  • 使用 Argilla 预测和评估结果。

  • (额外)将您的注释格式化回 Setfit ABSA。

简介#

情感分析是一种有效的自然语言处理方法,用于评估文本的情感基调。然而,衡量整体情感可能具有挑战性。方面级情感分析 (ABSA) 通过对文本中不同检测到的方面的情感进行分类来改进此任务。例如,考虑句子“食物很棒,但服务很糟糕”。在这种情况下,ABSA 识别出对“食物”的积极情感和对“服务”的消极情感。这种方法提供了对情感的详细理解,这对于捕捉消费者对特定产品或服务的更微妙的意见至关重要。

通常,ABSA 可以分为三个主要步骤

  1. 方面提取:此阶段涉及识别文本中提到的潜在方面或实体。在句子“食物很棒,但服务很糟糕”中,方面是“食物”和“服务”。

  2. 方面分类:这涉及验证已识别方面的准确性。也就是说,验证“食物”和“服务”是否确实是需要分析的方面。

  3. 极性分类:此步骤涉及评估与每个已识别方面相关联的情感。对“食物”和“服务”的情感是什么?

然而,Setfit ABSA 通过将方面和极性分类步骤合并为一个步骤来简化此过程。本教程将探讨这种简化方法的具体细节。

setfit-absa.png

运行 Argilla#

对于本教程,您需要运行 Argilla 服务器。部署和运行 Argilla 有两个主要选项

在 Hugging Face Spaces 上部署 Argilla:如果您想使用外部 Notebook(例如 Google Colab)运行教程,并且您在 Hugging Face 上拥有帐户,则只需点击几下即可在 Spaces 上部署 Argilla

deploy on spaces

有关配置部署的详细信息,请查看 官方 Hugging Face Hub 指南

使用 Argilla 的快速入门 Docker 镜像启动 Argilla:如果您想在 本地计算机上运行 Argilla,建议使用此选项。请注意,此选项仅允许您在本地运行教程,而不能与外部 Notebook 服务一起运行。

有关部署选项的更多信息,请查看文档的部署部分。

提示

本教程是一个 Jupyter Notebook。有两种运行它的选项

  • 使用此页面顶部的“在 Colab 中打开”按钮。此选项允许您直接在 Google Colab 上运行 Notebook。不要忘记将运行时类型更改为 GPU 以加快模型训练和推理速度。

  • 单击页面顶部的“查看源代码”链接下载 .ipynb 文件。此选项允许您下载 Notebook 并在本地计算机或您选择的 Jupyter Notebook 工具上运行它。

设置环境#

要完成本教程,您需要使用 pip 安装 Argilla 客户端和一些第三方库

[ ]:
%pip install argilla setfit[absa] spacy
# %pip install --upgrade huggingface_hub # Uncomment this line if you are not using the latest version of huggingface_hub

!spacy download en_core_web_lg

让我们进行所需的导入

[ ]:
import argilla as rg
import spacy
from datasets import load_dataset, Dataset
from setfit import AbsaTrainer, AbsaModel, TrainingArguments
from argilla.client.feedback.schemas import SpanValueSchema

如果您使用 Docker 快速入门镜像或公共 Hugging Face Spaces 运行 Argilla,则需要使用 URLAPI_KEY 初始化 Argilla 客户端

[3]:
# Replace api_url with the url to your HF Spaces URL if using Spaces
# Replace api_key if you configured a custom API key
# Replace workspace with the name of your workspace
rg.init(
    api_url="https://#:6900",
    api_key="argilla.apikey",
    workspace="argilla"
)

如果您运行的是私有 Hugging Face Space,您还需要按如下方式设置 HF_TOKEN

[ ]:
# # Set the HF_TOKEN environment variable
# import os
# os.environ['HF_TOKEN'] = "your-hf-token"

# # Replace api_url with the url to your HF Spaces URL
# # Replace api_key if you configured a custom API key
# rg.init(
#     api_url="https://[your-owner-name]-[your_space_name].hf.space",
#     api_key="admin.apikey",
#     extra_headers={"Authorization": f"Bearer {os.environ['HF_TOKEN']}"},
# )

启用遥测#

我们从您与教程的互动中获得宝贵的见解。为了改进我们自己,为您提供最合适的内容,使用以下代码行将帮助我们了解本教程是否有效地为您服务。虽然这是完全匿名的,但如果您愿意,可以选择跳过此步骤。有关更多信息,请查看 遥测 页面。

[ ]:
try:
    from argilla.utils.telemetry import tutorial_running
    tutorial_running()
except ImportError:
    print("Telemetry is introduced in Argilla 1.20.0 and not found in the current installation. Skipping telemetry.")

准备数据集#

对于我们的 ABSA 任务,我们将使用 semeval-absa,这是一个来自 SemEval-2015 Task 12 的数据集,其中包含基于方面的情感分析注释。它包含来自餐厅和笔记本电脑领域的评论,尽管在本教程中我们将重点关注餐厅领域。

首先,让我们加载数据集并显示一些示例。

[4]:
# Load the dataset
hf_dataset = load_dataset("jakartaresearch/semeval-absa", "restaurant", split="train", trust_remote_code=True)
hf_dataset[0]
[4]:
{'id': '3121',
 'text': 'But the staff was so horrible to us.',
 'aspects': {'term': ['staff'],
  'polarity': ['negative'],
  'from': [8],
  'to': [13]},
 'category': {'category': ['service'], 'polarity': ['negative']}}

接下来,我们将从数据集中随机选择 150 个示例,并将它们拆分为训练集和验证集。

[5]:
# Sample the dataset
hf_dataset_sample = hf_dataset.shuffle(seed=5).select(range(150))
[11]:
# Split the dataset into training and development sets
train_eval_split = hf_dataset_sample.train_test_split(test_size=0.3)

# Converting the training and development datasets to pandas DataFrames
train_df = train_eval_split['train'].to_pandas()
eval_df = train_eval_split['test'].to_pandas()

现在,是时候为 Setfit ABSA 格式化数据集了。数据集应具有四列:textspanlabelordinaltext 列包含要分析的文本,span 是方面,label 指的是方面的情感,ordinal 指示跨度出现的索引(如果多次出现,例如,如果“食物”在评论中出现 3 次,但仅在第二次提及时被识别为方面,则序号将为 1)。那么,如果评论有多个方面会发生什么?在这种情况下,我们将为每个方面复制评论。例如,

  • text:“食物很棒,但服务很糟糕”,span:“食物”,label:“positive”,ordinal:0

  • text:“食物很棒,但服务很糟糕”,span:“服务”,label:“negative”,ordinal:0

[44]:
# Helper function to calculate the ordinal of the term in the text
def calculate_ordinal(text, term, from_index):
    ordinal = 0
    start = 0

    while start < from_index:
        found_index = text.find(term, start)
        if found_index == -1 or found_index >= from_index:
            break
        ordinal += 1
        start = found_index + len(term)
    return ordinal

# Function to prepare the dataset for training
def prepare_dataset(df):
    prepared_data = []
    for _, row in df.iterrows():
        text, aspects = row['text'], row['aspects']
        for term, polarity, start_index in zip(aspects['term'], aspects['polarity'], aspects['from']):
            if polarity not in ['positive', 'negative']: # Skip neutral polarity
                continue
            prepared_data.append({
                "text": text,
                "span": term,
                "label": polarity,
                "ordinal": calculate_ordinal(text, term, start_index)
            })
    return prepared_data

# Helper function to convert a list of dictionaries to a dictionary of lists
def list_dict_to_dict_list(list_dict):
    return {key: [dic[key] for dic in list_dict] for key in list_dict[0]}
[13]:
# Formatting the data
prepared_data_train = prepare_dataset(train_df)
prepared_data_eval = prepare_dataset(eval_df)

# Creating Datasets
train_dataset = Dataset.from_dict(list_dict_to_dict_list(prepared_data_train))
eval_dataset = Dataset.from_dict(list_dict_to_dict_list(prepared_data_eval))
[14]:
train_dataset, eval_dataset
[14]:
(Dataset({
     features: ['text', 'span', 'label', 'ordinal'],
     num_rows: 90
 }),
 Dataset({
     features: ['text', 'span', 'label', 'ordinal'],
     num_rows: 47
 }))

最后,我们准备好了用于训练的数据集。

[15]:
train_dataset.to_pandas().head()
[15]:
文本 跨度 标签 序号
0 对他们的清酒马提尼不太感冒。 清酒马提尼 负面 0
1 扎实的葡萄酒单,知识渊博的员工,友好的... 葡萄酒单 正面 0
2 扎实的葡萄酒单,知识渊博的员工,友好的... 员工 正面 0
3 扎实的葡萄酒单,知识渊博的员工,友好的... 业主 正面 0
4 扎实的葡萄酒单,知识渊博的员工,友好的... 菜单 正面 0

使用 Setfit ABSA 进行训练#

正如我们之前提到的,Setfit ABSA 通过将方面和极性分类步骤合并为一个步骤来简化 ABSA 任务。此外,由于它基于 SetFit 架构,因此它是一个少样本模型,可以使用一些示例进行微调。

训练过程需要 15 分钟以上才能完成,具体取决于您机器的计算能力和设置的训练参数。但是,我们强烈建议使用 GPU 来加快训练过程。

[ ]:
# Check your GPU availability
import torch
if torch.cuda.is_available():
    device = torch.device("cuda")
    print(f"Using {torch.cuda.get_device_name(0)}")
elif torch.backends.mps.is_available():
    device = torch.device("mps")
else:
    device = torch.device("cpu")
    print("No GPU available, using CPU instead.")

训练过程很简单。最初,我们需要加载 Sentence Transformers 模型,以用于方面和极性分类。第一个模型将针对方面分类进行微调,第二个模型将针对每个检测到的方面的极性分类进行微调。可以使用相同的模型来完成这两项任务,也可以为每项任务选择不同的模型。默认情况下,用于方面提取的 spaCy 模型是 en_core_web_lg,尽管也可以使用其他模型。

我们将对这两项任务使用默认参数。但是,您可以通过设置 polarity_args 为每个任务自定义它们。有关更多信息,请查看 Setfit ABSA 文档

[ ]:
# Load the models
model = AbsaModel.from_pretrained(
    "sentence-transformers/paraphrase-mpnet-base-v2",
    "sentence-transformers/paraphrase-MiniLM-L6-v2",
    spacy_model="en_core_web_lg",
)

# Define the training arguments
args = TrainingArguments(
    output_dir="models",
    num_epochs=1,
    batch_size=16,
    evaluation_strategy="steps",
)

让我们训练模型!

[ ]:
# Initialize the trainer
trainer = AbsaTrainer(
    model,
    args=args,
    train_dataset=train_dataset,
    eval_dataset=eval_dataset,
)
trainer.train()

现在,我们可以在评估数据集上评估模型。输出将指示方面和极性分类任务的准确性。

[24]:
# Evaluate the model
metrics = trainer.evaluate(eval_dataset)
metrics
***** Running evaluation *****
***** Running evaluation *****
{'aspect': {'accuracy': 0.8235294117647058}, 'polarity': {'accuracy': 0.9090909090909091}}
[23]:
# Save the classification models locally or push them to the Hub
model.save_pretrained(
    "models/setfit-absa-aspect",
    "models/setfit-absa-polarity"
)

# model.push_to_hub(
#     "[hf-repo]/setfit-absa-aspect",
#     "[hf-repo]/setfit-absa-polarity"
# )

使用我们的模型进行推理#

结果很有希望!现在,让我们使用训练好的模型进行一些预测。

对于我们的测试数据,我们将使用来自同一领域的不同数据集 filtered_yelp_restaurant_reviews 中的 50 个示例。此数据集包括每个评论的总体情感,使我们能够创建一个全面的数据集,同时支持 ABSA 和一般情感分析。

[5]:
# Load the test dataset
test_dataset = load_dataset("vincha77/filtered_yelp_restaurant_reviews", split="test")
[6]:
# Sample the test dataset
test_dataset_sample = test_dataset.shuffle(seed=5).select(range(20))
test_dataset_sample
[6]:
Dataset({
    features: ['text', 'label', 'review_length'],
    num_rows: 20
})
[17]:
# Define the sentences and labels
labels = [label for label in test_dataset_sample['label']]
sentences = [text for text in test_dataset_sample['text']]

# Helper dictionaries
id2label_overall = {0: "NEG", 1: "NEU", 2: "POS"}
id2label_span = {"negative": "NEG", "positive": "POS"}
[8]:
sentences[0], labels[0]
[8]:
("I'm spoiled when it comes to Chinese food because I work in University City and can have my favorite Chinese whenever I want it. I'm giving King's a somewhat generous four stars with the understanding that they are pretty good for being outside of center city's limits.\n\nMy husband and I split a takeout order of cashew chicken (tasty but the celery was weird and raw), steamed vegetable dumplings (a bit on the doughy side but okay) and vegetable lo mein (pretty nice). Overall, the meal was around $20 and way more than enough for two people with lots of leftovers. It's definitely a great value. I think we've found our local Chinese takeout spot. What should we order next?",
 2)

获得测试数据后,我们可以使用训练好的模型进行预测。预测将包括每个跨度的方面和情感。

[ ]:
# Load the models
model = AbsaModel.from_pretrained(
    "models/setfit-absa-model-aspect",
    "models/setfit-absa-model-polarity"
)

# Make predictions
predictions = model.predict(sentences)
[11]:
predictions[0]
[11]:
[{'span': 'food', 'polarity': 'positive'},
 {'span': 'takeout order', 'polarity': 'positive'},
 {'span': 'cashew chicken', 'polarity': 'positive'},
 {'span': 'celery', 'polarity': 'positive'},
 {'span': 'vegetable dumplings', 'polarity': 'positive'},
 {'span': 'vegetable lo mein', 'polarity': 'positive'},
 {'span': 'meal', 'polarity': 'positive'},
 {'span': 'people', 'polarity': 'positive'},
 {'span': 'leftovers', 'polarity': 'positive'},
 {'span': 'value', 'polarity': 'positive'},
 {'span': 'takeout spot', 'polarity': 'positive'}]

评估预测结果#

最后,我们可以使用 Argilla 来评估模型的预测。监控模型性能对于检测需要改进的领域或创建新的 ABSA 数据集至关重要。例如,在我们的场景中,测试数据集中的评论比训练集中的评论更长,这可能影响模型的有效性。

因此,我们创建一个反馈数据集,其中包含用于方面和情感的 SpanQuestion 和用于总体情感的 LabelQuestionSpanQuestion 将允许我们选择特定字段的文本部分并为其应用标签。LabelQuestion 将要求注释者从选项列表中选择一个标签。

有关创建反馈数据集和新的跨度问题的更多信息,请查看 Argilla 文档

[ ]:
# Define the feedback dataset
rg_dataset = rg.FeedbackDataset(
    fields=[
        rg.TextField(name="text"),
        rg.TextField(name="aspect-based-sentiment-analysis"),
    ],
    questions=[
        rg.LabelQuestion(
            name="overall-sentiment",
            title="What is the overall sentiment of the text?",
            labels={"POS": "Positive", "NEU":"Neutral", "NEG": "Negative"},
            required=True,
        ),
        rg.SpanQuestion(
            name="aspect-polarity",
            title="Highlight the aspects and their polarity in the text:",
            labels={"POS", "NEG"},
            field="aspect-based-sentiment-analysis",
            required=True
        ),
    ],
    guidelines="Please, read the question carefully and try to answer it as accurately as possible."
)
rg_dataset = rg_dataset.push_to_argilla(name="absa-dataset", workspace="argilla")

然后,我们必须将记录添加到我们的反馈数据集。预测将作为响应添加,以方便注释者的工作。

请记住,由于它们是作为响应添加的,因此记录将显示为 submitted

[23]:
# Helper function to find the span indices in the sentence and return a list of SpanValueSchema objects
nlp = spacy.load("en_core_web_lg")

def find_span_indices(sentence, predictions):
    doc = nlp(sentence)
    found_spans = []

    last_found_index = {span['span']: -1 for span in predictions}

    for span_dict in predictions:
        span_text = span_dict['span']
        found = False

        for i in range(len(doc)):
            window_text = " ".join(doc[j].text for j in range(i, min(i + len(span_text.split()), len(doc))))

            if window_text == span_text and i > last_found_index[span_text]:
                start_index = doc[i].idx
                end_index = doc[i + len(span_text.split()) - 1].idx + len(doc[i + len(span_text.split()) - 1])

                found_spans.append(SpanValueSchema(start=start_index, end=end_index,
                                                label=id2label_span[span_dict['polarity']]))
                last_found_index[span_text] = i
                found = True
                break
        if not found:
            raise ValueError(f"Span '{span_text}' not found in the sentence.")

    return found_spans
[ ]:
# Add the records to the dataset
records = [
    rg.FeedbackRecord(
        fields={
            "text": sentence,
            "aspect-based-sentiment-analysis": sentence,
        },
        responses=[
            {
                "values": {
                    "overall-sentiment": {
                        "value": id2label_overall[label]
                    },
                    "aspect-polarity": {
                        "value": find_span_indices(sentence, prediction),
                    },
                }
            }
        ]
    )
    for sentence, prediction, label in zip(sentences, predictions, labels)
]
rg_dataset.add_records(records)

setfit-basa-argilla.png

(额外)为 Setfit ABSA 格式化注释#

获得注释后,您可以重新格式化它们以用于 Setfit ABSA,例如,重新训练模型以合并这些新注释。

在我们的场景中,我们将像预测是准确的一样进行操作。因此,我们首先检索注释数据集,并专注于已提交的评论。

[26]:
# Retrieve the annotated dataset and filter the records
annotated_dataset = rg.FeedbackDataset.from_argilla(name="absa-dataset", workspace="argilla")
filtered_dataset = annotated_dataset.filter_by(response_status="submitted")

最后,我们将遍历记录,为 Setfit ABSA 格式化它们。与我们在训练阶段的方法类似,我们将为每个已识别的方面和相应的情感复制评论。在此过程中,我们将仅考虑方面及其关联的匹配情感。

[42]:
# Helper function to get the span text
def get_span_text(text, start, end):
    return text[start:end]

def prepare_absa_dataset(records):
    data = []
    for record in records:
        text = record.fields["text"]

        for response in record.responses:
            overall_sentiment = response.values["overall-sentiment"].value

            for aspect_details in response.values["aspect-polarity"].value:
                aspect_text = get_span_text(text, aspect_details.start, aspect_details.end)
                data.append({
                    "text": text,
                    "span": aspect_text,
                    "label": aspect_details.label,
                    "ordinal": calculate_ordinal(text, aspect_text, aspect_details.start),
                    "overall": overall_sentiment
                })
    return data
[ ]:
# Formatting the data
records = filtered_dataset.records
absa_data = prepare_absa_dataset(records)

# Create the dataset
absa_dataset = Dataset.from_dict(list_dict_to_dict_list(absa_data))
[48]:
absa_dataset.to_pandas().head()
[48]:
文本 跨度 标签 序号 总体
0 在吃中餐方面,我被宠坏了,因为... 食物 正面 0 正面
1 在吃中餐方面,我被宠坏了,因为... 外卖订单 正面 0 正面
2 在吃中餐方面,我被宠坏了,因为... 腰果鸡丁 正面 0 正面
3 在吃中餐方面,我被宠坏了,因为... 芹菜 正面 0 正面
4 在吃中餐方面,我被宠坏了,因为... 蔬菜饺子 正面 0 正面

结论#

在本教程中,我们深入研究了使用 Setfit ABSA 和 Argilla 进行方面级情感分析 (ABSA) 的任务。我们的旅程始于调整数据集以适应特定格式,然后微调句子转换器。然后,我们继续使用这些模型生成预测。为了衡量我们模型的有效性,我们使用 Argilla 进行性能评估。我们的最后一步是重新格式化收集的注释,使其与 Setfit ABSA 兼容,从而结束了我们的 ABSA 工作流程的循环。