SemScore:使用语义相似性评估LLM

社区文章 发布于2024年3月9日

image/jpeg 准确评估大型语言模型 (LLM) 的性能至关重要,但也很困难。目前的评估方法存在显著局限性。

  • 人工评估,如 LMSYS Arena 中的评估,是黄金标准,但速度慢。
  • 基准测试,如 MMLU,可能被作弊
  • 使用另一个 LLM,如 GPT-4 作为评判者,成本高昂,并且可能存在偏见

本文探讨了最近引入的一种评估 LLM 的方法 SemScore,该方法通过查看其答案的语义来评估 LLM。

image/png 这篇博客文章介绍了这个想法、它的用处以及如何将其应用于您自己的模型和训练运行。

第一部分:什么是 SemScore,我为什么会关心它?

SemScore 是最近一项发表中提出的方法,它侧重于使用嵌入(embeddings)来分析模型输出的语义内容

嵌入是将文本转换为携带语义意义的数值表示。从文本到嵌入向量的转换是通过嵌入模型完成的。

为了说明这一点,考虑单词orangelemoncarmoney。使用sentence-transformers/all-mpnet-base-v2(SemScore 论文中使用的模型)嵌入单词orange会产生一个 768 维的向量。

tensor([[ 3.2832e-02,  2.2214e-02,  9.9305e-02, -1.0286e-01,  5.2077e-03,
 -5.9724e-02, -1.8181e-01,  6.0466e-02, -8.1715e-03, -5.3353e-02,
          2.1441e-02, -7.4530e-02,  7.7298e-02, -7.2748e-02, -1.6974e-01,
         -2.5297e-01, -1.7442e-02,  3.8736e-02, -4.5297e-02, -1.0881e-01,
         ...
          -1.3494e-02,  2.7610e-02,  2.9820e-01,  1.8822e-02,  1.4104e-01,
          3.1662e-03,  2.3393e-34, -3.2049e-02, -1.1889e-01, -9.7884e-02,
          2.5336e-02, -2.4282e-02, -1.2387e-01,  3.2787e-01,  1.1333e-02,
          1.0318e-01, -8.3175e-02,  4.2550e-02]], device='cuda:0')

如果使用主成分分析 (PCA) 将这 768 个维度降至最重要的 2 个维度,这些词可以在 2D 图上可视化。

image/png

虽然这个图并不能准确反映这些词在所有 768 个维度上的差异,但我们仍然可以理解图上的距离反映了这些词的含义差异。

这与 LLM 的评估有什么关系? 嵌入不仅允许我们将简单的单词转化为有趣的图表,还可以通过使用余弦相似度来量化整个句子或段落的相似度。

余弦相似度是一种衡量两个向量相似程度的指标,无论它们的大小如何。可以将其想象成查看两个箭头之间的角度;这个角度越接近零,箭头(或向量)就越相似。余弦相似度为 1 意味着向量指向完全相同的方向(非常相似),0 意味着它们垂直(不相似),-1 意味着它们指向相反的方向(非常不相似)。

回到我们简单的例子,上述四个词之间的余弦相似度反映了它们的语义相似性。例如,lemonorange的余弦相似度高于lemoncar

  • lemon vs. orange: 0.534
  • lemon vs. car: 0.291
  • lemon vs money: 0.228
  • car vs. money: 0.341

将这个概念应用于整个 LLM 响应(而不是简单的词)就是 SemScore 的全部内容。

嵌入对话数据

让我们通过将嵌入应用于实际的对话数据,更接近一个实际用例。

下面是 Open Assistant 2 数据集中所有问题的可视化,同样使用 sentence-transformers/all-mpnet-base-v2 进行嵌入,并通过 PCA 从高维空间降维到二维。

image/png 为了验证嵌入文本段落(本例中为问题)的方法,让我们找出数据集中最相似(余弦相似度最高)和最不相似(余弦相似度最低)的问题对。

image/png

数据集中最相似的两个问题(去除重复项后)

问题 1

我希望你扮演 Linux 终端。我将输入命令,你将回复终端应显示的内容。我只想让你在唯一的代码块中回复终端输出,不包含其他内容。不要写解释。除非我指示你,否则不要输入命令。当我需要用英语告诉你一些事情时,我会将其放在花括号中 {像这样}。我的第一个命令是 pwd。

问题 2

我希望你扮演 Linux 终端。我将输入命令,你将回复终端应显示的内容。我只想让你在唯一的代码块中回复终端输出,不包含其他内容。不要写解释。除非我指示你,否则不要输入命令。当我需要用英语告诉你一些事情时,我会将其放在花括号中 {像这样}。我的第一个命令是 pwd。

这基本上是同一个问题,唯一的区别是第三句中的“a”被第二个问题中的“one”取代。

两个最不相似的问题

问题 1

这些是我喜欢的书,请给我推荐更多类似的

《使女的故事》作者玛格丽特·阿特伍德

《一个平衡的世界》作者罗欣顿·米斯特里

《受教育》作者塔拉·韦斯特弗

《埃莉诺·奥利芬特完全没事》作者盖尔·霍尼曼

《爱的艺术》作者阿兰·德·波顿

《一个叫奥维的男人》作者弗雷德里克·巴克曼

《五年之约》作者丽贝卡·塞尔

《小火星》作者塞莱斯特·吴

《普通人》作者莎莉·鲁尼

《房间》作者艾玛·多诺霍

问题 2

巨型霸王龙和哥斯拉哪个会赢?

我想我们都同意,这两个请求确实非常不同。

嵌入 LLM 答案以重现竞技场排行榜

让我们升级一下游戏,看看如何将其应用于 LLM 的基准测试。

在 SemScore 论文中,作者通过计算 LLM 答案(预测)与人类答案(参考)的相似度,重建了一个 LLM 排名。

image/png

现在我们将做类似的事情,将 SemScore 应用于一个带有人类判断的对话语料库:LMSYS 竞技场对话。竞技场对话通过直接比较评估 LLM 生成的答案,用户提出问题并从不同的 LLM 收到两个响应。在不知道哪个 LLM 提供了哪个答案的情况下,用户指定他或她更喜欢哪个响应。根据这些人类评分,为每个模型计算 ELO 分数,排名给出了反映人类偏好的全球排行榜

我们使用 LMSYS 竞技场数据集的方法与 SemScore 论文中发表的方法不同。我们没有问题的参考答案,而是对相同问题有两个并排的答案。为了应用 SemScore,我们需要将模型生成的答案与参考答案进行比较。假设 GPT-4 提供最佳答案,我们将每个模型的答案与 GPT-4 的答案进行比较。模型的答案与 GPT-4 答案越相似,排名就越高。

我们先加载 LMSYS 竞技场对话。数据集结构如下:

Dataset({
    features: ['question_id', 'model_a', 'model_b', 'winner', 'judge', 'conversation_a', 'conversation_b', 'turn', 'anony', 'language', 'tstamp', 'openai_moderation', 'toxic_chat_tag'],
    num_rows: 33000
})

第一个条目

{
  "question_id": "58210e39b3fd4441a2bd4a518bb44c2d",
  "model_a": "chatglm-6b",
  "model_b": "koala-13b",
  "winner": "model_b",
  "judge": "arena_user_973",
  "conversation_a": [
    {
      "content": "What is the difference between OpenCL and CUDA?",
      "role": "user"
    },
    {
      "content": "OpenCL and CUDA are two different programming models that are used for parallel computing.OpenCL is a general-purpose\u5e76\u884c\u7f16\u7a0b\u63a5\u53e3 that allows developers to write parallel code that can run on any platform that supportsCL, which includes most modern operating systems and computer systems, including Windows, Linux, and macOS. It provides a lower-level, more flexible API that is more suitable for building large-scale distributed computing systems.CUDA is a specific implementation ofOpenCL that is designed for performance and scalability in devices with multiple GPU(s). It was developed by Nvidia and is widely used for scientific computing, machine learning, and\u56fe\u5f62\u56fe\u50cf\u5904\u7406. The CUDA API provides a high-level, more efficient way to write parallel code that can run on top of a GPU, while still maintaining the ability to write code that can be easily adapted for other parallel computing platforms.",
      "role": "assistant"
    }
  ],
  "conversation_b": [
    {
      "content": "What is the difference between OpenCL and CUDA?",
      "role": "user"
    },
    {
      "content": "OpenCL and CUDA are both programming languages for parallel computing on GPUs, but they differ in several key ways:\n\n1.   Architecture: OpenCL is a general-purpose parallel computing language, while CUDA is designed specifically for GPU computing. OpenCL can run on various types of processors, including CPUs, GPUs, and FPGAs, whereas CUDA is only compatible with NVIDIA GPUs.\n2.   Language: OpenCL is a C++ language, while CUDA is a C++ language with additional features for GPU computing. CUDA provides additional libraries and tools for handling graphics and other tasks unique to GPUs.\n3.   Driver support: OpenCL has a wider range of device support, with support for not just NVIDIA GPUs but also other manufacturers like AMD, Intel, and ARM. CUDA is only available on NVIDIA GPUs, and requires the CUDA Toolkit to be installed on the host machine.\n4.   Memory management: OpenCL provides a unified memory model, where all the data is stored in global memory, and the kernel can access any part of it. CUDA, on the other hand, uses a hierarchical memory architecture, where data is divided into host, device, and shared memory.\n5.   Performance: OpenCL and CUDA have different strengths and weaknesses in terms of performance. OpenCL is generally faster for numerical computations, while CUDA is faster for graphics and other tasks that require fast memory access. CUDA also has better support for 64-bit floating-point operations, which are essential for some scientific applications.\n\nIn summary, OpenCL and CUDA are both programming languages for parallel computing on GPUs, but they differ in terms of architecture, language, driver support, memory management, and performance. OpenCL is a general-purpose language that can run on various types of processors, while CUDA is specific to NVIDIA GPUs and provides additional libraries and tools for handling graphics and other tasks unique to GPUs.",
      "role": "assistant"
    }
  ],
  "turn": 1,
  "anony": true,
  "language": "English",
  "tstamp": 1682351591.1322,
  "openai_moderation": {
    "categories": {
      "harassment": false,
      "harassment/threatening": false,
      "hate": false,
      "hate/threatening": false,
      "self-harm": false,
      "self-harm/instructions": false,
      "self-harm/intent": false,
      "sexual": false,
      "sexual/minors": false,
      "violence": false,
      "violence/graphic": false
    },
    "category_scores": {
      "harassment": 2.8765102e-05,
      "harassment/threatening": 5.663866e-07,
      "hate": 5.574919e-06,
      "hate/threatening": 2.3965333e-08,
      "self-harm": 5.1901172e-09,
      "self-harm/instructions": 1.1517327e-08,
      "self-harm/intent": 3.9070875e-09,
      "sexual": 4.381485e-06,
      "sexual/minors": 5.541973e-08,
      "violence": 2.2985896e-06,
      "violence/graphic": 4.8212314e-07
    },
    "flagged": false
  },
  "toxic_chat_tag": {
    "roberta-large": {
      "flagged": false,
      "probability": 0.008313022553920746
    },
    "t5-large": {
      "flagged": false,
      "score": 7.943665219245499e-05
    }
  }
}

接下来,我们提取所有与我们的参考模型 GPT-4 进行比较的答案。这使得每个模型拥有的答案数量如下。

image/png

让我们看看数据集中的一个示例条目

问题 “我的狗吃了些巧克力。我该怎么办?”

Vicuna-13B 的答案:

如果你的狗吃了巧克力,你首先应该尝试确定它吃了多少巧克力以及巧克力的类型。巧克力越黑越苦,对你的狗来说就越危险。

如果你的狗只吃了少量巧克力,它们可能根本不会表现出任何症状。但是,如果它们吃了大量巧克力,它们可能会出现呕吐、腹泻、心率加快和躁动等症状。在严重的情况下,巧克力中毒可能导致癫痫发作、昏迷,甚至死亡。

如果你怀疑你的狗吃了巧克力,立即联系兽医很重要。你的兽医将能够确定情况的严重程度并提供适当的治疗。在某些情况下,你的兽医可能会建议催吐以清除狗胃中的巧克力。在其他情况下,他们可能需要服用活性炭或其他药物以帮助清除狗体内的巧克力。

一般来说,最好将巧克力放在狗够不到的地方,以防止意外摄入。如果你对某种巧克力是否对你的狗安全有任何疑问,最好咨询你的兽医。

GPT-4 的答案:

我不是兽医,但如果你的狗吃了巧克力,立即采取行动很重要,因为巧克力对狗有毒。以下是一些你可能需要考虑的步骤:

  1. 确定你的狗食用了多少巧克力以及巧克力的类型。黑巧克力和烘焙巧克力比牛奶巧克力毒性更大。
  2. 检查巧克力中毒的症状,可能包括呕吐、腹泻、呼吸急促、心率加快和癫痫发作。
  3. 立即致电你的兽医或宠物中毒热线。他们可以根据具体情况提供指导。
  4. 遵循兽医或热线的建议,可能包括催吐、服用活性炭或带你的狗去看兽医进行治疗。

请记住咨询专业人士,以获取针对你狗狗具体情况的建议。

这两个答案的余弦相似度为 0.958,表明相似度很高。如果我们将 Vicuna-13B 的所有答案与相应的 GPT-4 答案的相似度进行平均,我们得到 Vicuna-13B 的 SemScore 为 0.770

通过计算数据集中所有模型的平均分数,我们可以(几乎)重现基于人工评估的官方竞技场排行榜。下图中显示了 SemScore 与 ELO 评分的相关性,该评分基于竞技场中收集的人工评估。

image/png

ELO 评分和 SemScore 的相关性良好,但并非完美。让我们看看排名如何相关以及有哪些差异。

image/png

我没有详细说明为什么 wizardlm-13bfastchat-t5-3b 模型在 SemScore 方面排名更高(而 palm-2 更低),而不是根据人类判断,但存在良好的相关性,表明 SemScore 是另一个评估 LLM 答案的有用工具。如果你想重现上述结果,代码已在笔记本中提供。

在下一部分中,我们将详细介绍如何使用代码评估任何 Hugging Face 模型在对话数据集上的表现。

第二部分:实施 - 让 SemScore 变为现实

让我们看看如何在训练后和训练期间使用 SemScore 评估 Hugging Face 模型和数据集。

先决条件

请务必使用最新版本的 Hugging Face 套件,以下是我用于后续代码的特定包版本。

accelerate                        0.28.0            
bitsandbytes                      0.42.0
datasets                          2.18.0
flash-attn                        2.5.6
peft                              0.9.0
sentencepiece                     0.2.0
transformers                      4.38.2
trl                               0.7.11
torch                             2.2.1

“Hello World”示例

下面的代码片段演示了如何加载嵌入模型并计算简单句子之间的语义相似度。

代码主要取自 SemScore 论文中使用的嵌入模型 sentence-transformers/all-mpnet-base-v2 的模型卡。

from transformers import AutoTokenizer, AutoModel
import torch
import torch.nn.functional as F

#Mean Pooling - Take attention mask into account for correct averaging
def mean_pooling(model_output, attention_mask):
    token_embeddings = model_output[0] #First element of model_output contains all token embeddings
    input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
    return torch.sum(token_embeddings * input_mask_expanded, 1) / torch.clamp(input_mask_expanded.sum(1), min=1e-9)

# Load model from HuggingFace Hub
tokenizer = AutoTokenizer.from_pretrained('sentence-transformers/all-mpnet-base-v2')
model = AutoModel.from_pretrained('sentence-transformers/all-mpnet-base-v2')

现在模型已加载,让我们获取嵌入向量。

# Sentences we want sentence embeddings for
sentences = ["apple", "orange", "car"]

# Tokenize sentences
encoded_input = tokenizer(sentences, padding=True, truncation=True, return_tensors='pt')

# Compute token embeddings
with torch.no_grad():
    model_output = model(**encoded_input)

# Perform pooling
sentence_embeddings = mean_pooling(model_output, encoded_input['attention_mask'])

# Normalize embeddings
sentence_embeddings = F.normalize(sentence_embeddings, p = 2, dim = 1)

为了获得余弦相似度,我们计算归一化向量的点积。

for i in range(0, len(sentences)):
    print(
        sentences[0],
        sentences[i],
        (sentence_embeddings[0] @ sentence_embeddings[i]).item()
    )    

苹果 苹果 0.9999999403953552

苹果 橙子 0.40115007758140564

苹果 汽车 0.3137194514274597

在任何数据集上评估微调模型

为了使评估更容易理解,我们为嵌入模型使用一个包装类。EmbeddingModelWrapper 默认加载 all-mpnet-base-v2,并公开 get_embeddings 函数用于计算字符串列表的嵌入,以及 get_similarities 函数用于计算两个嵌入向量列表的余弦相似度。其余代码主要取自 sentence-transformers/all-mpnet-base-v2 的模型卡。

class EmbeddingModelWrapper():
    DEFAULT_MODEL = "sentence-transformers/all-mpnet-base-v2"

    def __init__(self, model_path=DEFAULT_MODEL, bs=8):
        self.model, self.tokenizer = self.load_model(model_path)
        self.bs = bs
        self.cos = nn.CosineSimilarity(dim=1, eps=1e-6)

    def load_model(self, model_path):
        model = AutoModel.from_pretrained(model_path).cuda()
        tokenizer = AutoTokenizer.from_pretrained(model_path)
        return model.eval(), tokenizer

    def get_embeddings(self, sentences):
        ...
        
    def get_similarities(self, x, y = None):
        ...

EmbeddingModelWrapper 的完整代码请参见 semscore.py

这将使模型评估变得更加容易。例如,EmbeddingModelWrapper 可以用于重现之前的简单示例,如下所示:

from semscore import EmbeddingModelWrapper
em = EmbeddingModelWrapper()

words = ["lemon", "orange", "car", "money"]
embds = em.get_embeddings(words)

similarities = em.get_similarities(embds)

返回的 similarities 包含所有组合的余弦相似度。

tensor([[1.0000, 0.5340, 0.2909, 0.2281],
        [0.5340, 1.0000, 0.3027, 0.1968],
        [0.2909, 0.3027, 1.0000, 0.3411],
        [0.2281, 0.1968, 0.3411, 1.0000]])

我们现在可以测试 Hugging Face 模型了。

让我们以 TinyLlama 为例,并在 Open Assistant 数据集的随机子集上进行测试。我们加载数据集,用问题挑战 TinyLlama,并将答案与参考答案进行比较。

from datasets import load_dataset

dataset_name = "g-ronimo/oasst2_top1_en"
dataset = load_dataset(dataset_name)["train"]
dataset = dataset.shuffle(seed = 42).select(range(10))

此数据集中的一个条目示例如下:

{
  "conversation": [
    {
      "content": "Can photoshop be used as a tool to forge documents illegally?",
      "role": "user"
    },
    {
      "content": "Yes, Photoshop can be used to forge documents due to the application's ability to modify digital images. Users with access to a Photoshop tool would have the potential to alter documents and create highly convincing fakes. It is important to note that forging documents is not an intended goal of the photoshop tool, and forging documents is likely to be illegal or immoral based on a specific situation.",
      "role": "assistant"
    }
  ]
}

上述对话包含一个单轮,一个问题和一个答案,但此数据集中的对话也可能包含多轮。

接下来,加载 TinyLlama。

from transformers import AutoModelForCausalLM, AutoTokenizer
import torch

model_path="TinyLlama/TinyLlama-1.1B-Chat-v1.0"
model = AutoModelForCausalLM.from_pretrained(
    model_path,    
    device_map = "auto",
    torch_dtype = torch.bfloat16,
)
tokenizer = AutoTokenizer.from_pretrained(model_path, use_fast=False)

我们现在可以使用数据集中的问题来挑战 TinyLlama,并计算生成的答案与参考答案的语义相似度。

answers_ref, answers_pred = [], []

for conversation in dataset["conversation"]:
    for i, msg in enumerate(conversation):
        if msg["role"] == "assistant": continue
        partial_conv = conversation[0:i+1]

        prompt_tok = tokenizer.apply_chat_template(
            partial_conv, 
            add_generation_prompt = True, 
            return_tensors = "pt"
        ).to("cuda")
        answer_tok = model.generate(
            prompt_tok, 
            eos_token_id = tokenizer.eos_token_id,
            max_new_tokens = 2000, 
        )
        answer = tokenizer.decode(
            answer_tok[0][len(prompt_tok[0]):], 
            skip_special_tokens = True
        )

        answers_pred.append(answer) 
        answers_ref.append(conversation[i+1]["content"])  

回到 PS 问题,TinyLlama 是这样回答的:

问题:Photoshop 可以被用作非法伪造文件的工具吗?

TinyLlama 的答案:

是的,Photoshop 可以被用作非法伪造文件的工具。Photoshop 是一款功能强大的图像编辑软件,可以用来篡改图像和制作假文件。它可以用来制作假签名、地址以及其他非真实的文件。然而,需要注意的是,使用 Photoshop 伪造文件可能导致法律后果,最好避免将此软件用于此目的。

在某些情况下,Photoshop 可以用于创建与真实文档无法区分的虚假文档。然而,重要的是要注意该软件很容易被检测到,并可能导致法律后果。最好避免使用 Photoshop 伪造文档,而是使用其他合法方法来创建虚假文档。

总之,Photoshop 可以被用作非法伪造文件的工具,但重要的是要避免将此软件用于此目的,并使用其他合法方法来创建虚假文件。

参考答案:

是的,Photoshop 可以用来伪造文件,因为该应用程序能够修改数字图像。拥有 Photoshop 工具的用户有可能修改文件并创建高度逼真的假冒文件。重要的是要注意,伪造文件并非 Photoshop 工具的预期目标,并且根据具体情况,伪造文件很可能是非法或不道德的。

我们现在可以使用 EmbeddingModelWrapper 计算预测与参考答案的余弦相似度。

from semscore import EmbeddingModelWrapper
from statistics import mean

em = EmbeddingModelWrapper()
similarities = em.get_similarities(
    em.get_embeddings( answers_pred ),
    em.get_embeddings( answers_ref ),
)

对于上述 Photoshop 问题,预测答案与参考答案的余弦相似度为 0.92。我们从 OA 数据集中选择的全部 10 个问题的平均相似度为 0.71

如果我们将此扩大到 100 个问题并测试 TinyLlama 和 Mistral,结果如下:

模型 SemScore
TinyLlama/TinyLlama-1.1B-Chat-v1.0 0.6903450180861083
models/Mistral-7B-Instruct-v0.2 0.7476783179036983

我们来看一个 Mistral 在 SemScore 方面击败 TinyLlama 的例子。

问题:写一篇关于投资的推特帖子

参考答案

1/5 在过去的十年里,投资在公众视野中变得越来越重要,这在很大程度上要归功于 Robinhood 或 eToro 等面向普通人(即散户投资者)的应用程序。

2/5 虽然有许多在线“大师”和“专家”对日内交易和股票期权发表各种意见,但要获得可靠收益而无需将投资作为全职工作,只有一种真正的选择……

3/5 那就是构建一个由低风险、低回报、跨多个不同行业的多元化资产组成的投资组合,并长期持有。它不会迅速创造财富,但它会保障你的财务未来。

4/5 另一种降低投资风险的方法是经常小额投资。这可以让你避免在暂时下跌期间存入大量资金的风险。

5/5 这就是普通人一生投资所需的所有建议。记住,投资不应该是为了致富,而应该是为了将你的额外资源投入工作,为自己和他人创造一个更美好的未来。

答案 TinyLlama/TinyLlama-1.1B-Chat-v1.0 (相似度 0.603)

  1. 引言:首先介绍自己以及对投资的兴趣。解释为什么想了解更多关于这个话题,是什么激励了你,以及这些信息如何在经济上帮助你。使用 #investmenttips 或 #financialadvice 等标签来吸引其他对类似话题感兴趣的用户。

  2. 了解投资:讨论投资基础知识,包括股票、债券、共同基金和交易所交易基金 (ETFs) 之间的区别。提供成功投资者的例子,并解释他们如何通过健全的财务规划和纪律实现成功。使用 #stockmarketprimer 或 #investmentbasics 等相关标签。

  3. 多元化:讨论在不同资产类别(如股票、债券和房地产)之间分散投资组合的重要性。解释这种策略如何帮助降低风险并随着时间的推移最大化回报。使用 #diversifyyourportfolio 或 #assetclassdiversification 等相关标签。

  4. 风险管理:讨论投资中的风险管理概念,这涉及平衡潜在损失与预期收益。解释如何识别与特定资产和策略相关的风险,以及如何减轻这些风险。使用 #riskmanagement 或 #assetallocation 等相关标签。

  5. 市场周期:讨论市场周期,即高波动期后跟低波动期。解释这些周期如何影响个人投资,并提供在不确定时期管理风险的技巧。使用 #marketcycle 或 #volatilitiestoinvestors 等相关标签。

  6. 投资组合构建:根据您对以上几点的理解,制定一个构建有效投资组合的全面计划。

答案 Mistral-7B-Instruct-v0.2 (相似度 0.787)

🧵1/ 投资是创造财富和保障财务未来的重要组成部分。现在开始永远不嫌早或不嫌晚!#投资 #理财技巧

🧵2/ 在深入了解之前,了解基础知识至关重要:股票、债券、共同基金、ETF 等。研究每种投资类型,并确定哪种最符合您的目标、风险承受能力和时间范围。#股票 #债券 #ETF

🧵3/ 多元化是降低风险的关键。不要把所有的鸡蛋都放在一个篮子里!将投资分散到各种资产类别、行业和地域。#多元化 #风险管理

🧵4/ 为您的投资设定明确的目标。您是在寻求资本增值、收入生成,还是两者兼而有之?了解您的目标将有助于指导您的决策。#财务目标 #资本增值 #收入生成

🧵5/ 投资需要耐心。长期策略通常比短期投机带来更好的回报。记住,罗马不是一天建成的!#长期投资 #耐心有回报

🧵6/ 随时了解市场趋势、经济指标和公司业绩。在做出投资决策时,知识就是力量。#市场新闻 #经济指标 #公司业绩

🧵7/ 最后,考虑向值得信赖的财务顾问或专业人士寻求建议。他们可以根据自己的专业知识和经验提供宝贵的见解和指导。#财务顾问 #专业建议

帖子结束 🧵 #投资101 #财富积累 #个人理财 #金钱事宜 #金融

如果您想查看更多示例,请查看完整结果此处。代码已收集在笔记本中。

我们可以将分析扩展到包含数据集中的更多样本并测试其他模型,但结果将难以解释。这是一个流行的数据集,模型可能至少在其中一部分上进行了训练。

为了解决这个问题,一种方法是使用 SemScore 在训练后评估您自己的模型。通过在数据集的验证集上计算 SemScore,您可以深入了解您的模型是否从训练集中泛化良好,并且正在生成与您期望输出语义相似的答案。

此外,SemScore 可以成为在训练期间监控进度的宝贵工具,我将在下一节中进行演示。

在训练期间进行评估

训练 LLM,尤其是在对话数据上,通常会导致评估损失(下图中 eval/loss)随时间增加——这种现象通常与过拟合相关。然而,这种增加并不总是与自然语言任务性能的下降相关。

image/jpeg

SemScore 提供了一种在整个训练过程中监控预测与参考答案的语义相似性的方法,从而提供对模型真实进展的洞察,超越了传统损失指标所能提供的范围。

让我们将 SemScore 融入典型的基于 Hugging Face 的训练运行中。以下是在 Open Assistant 数据集的(去重)版本上训练 TinyLlama 的一个最小示例。

如果你熟悉使用 Hugging Face 套件微调模型,这看起来会很熟悉,而且你可能有自己的代码和例程设置,可能会有所不同。下面的代码是 TinyLlama 的完整微调,它有效地使用 24 GB 显存的 GPU(如 NVIDIA GeForce RTX 3090 或 4090)在大约 1.5 小时内完成一个像样的聊天机器人的训练。

from transformers import AutoTokenizer, AutoModelForCausalLM, TrainingArguments, BitsAndBytesConfig, set_seed
from peft import LoraConfig
from trl import SFTTrainer, setup_chat_format, DataCollatorForCompletionOnlyLM
from datasets import load_dataset
import torch

set_seed(42)

modelpath = "TinyLlama/TinyLlama-1.1B-intermediate-step-1431k-3T"
model = AutoModelForCausalLM.from_pretrained(
    modelpath,    
    device_map = "auto",
    torch_dtype = torch.bfloat16,
)
tokenizer = AutoTokenizer.from_pretrained(modelpath, use_fast = False)

model, tokenizer = setup_chat_format(model, tokenizer)
if tokenizer.pad_token in [None, tokenizer.eos_token]: 
    tokenizer.pad_token = tokenizer.unk_token

dataset = load_dataset("g-ronimo/oasst2_top4k_en")

training_arguments = TrainingArguments(
    output_dir = "out_OA_TL",
    evaluation_strategy = "steps",
    label_names = ["labels"],
    per_device_train_batch_size = 16,
    gradient_accumulation_steps = 1,
    save_steps = 250,
    eval_steps = 250,
    logging_steps = 1, 
    learning_rate = 1e-5,
    num_train_epochs=10,
    lr_scheduler_type = "constant",
    optim = 'paged_adamw_32bit',
    bf16 = True,
    gradient_checkpointing = True,
    group_by_length = True,
)

trainer = SFTTrainer(
    model = model,
    tokenizer = tokenizer,
    train_dataset = dataset["train"],
    eval_dataset = dataset['test'],
    data_collator = DataCollatorForCompletionOnlyLM(
        instruction_template = "<|im_start|>user", 
        response_template = "<|im_start|>assistant", 
        tokenizer = tokenizer, 
        mlm = False),
    max_seq_length = 512,
    args = training_arguments,
)```

At this point you would start training with `trainer.train()`, but before that we will plugin a `TrainerCallback` that performs the SemScore evaluation after each epoch.

`SemscoreEvalCallback` loads `ModelPredictionGenerator`, a helper class for batch inference (source) to generate answers and then passes the generated answers and references to `EmbeddingModelWrapper` which calculates and logs the cosine similarities after each epoch. To run this, please obtain [semscore.py](https://github.com/geronimi73/semscore/blob/main/semscore.py) which contains definitions of `ModelPredictionGenerator` and `EmbeddingModelWrapper`.

```python
from transformers import TrainerCallback
from statistics import mean

from semscore import ModelPredictionGenerator, EmbeddingModelWrapper

class SemscoreEvalCallback(TrainerCallback):
    def on_evaluate(self, args, state, control, model, tokenizer, eval_dataloader, **kwargs):

        generator = ModelPredictionGenerator(model = model, tokenizer = tokenizer)
        eval_ds = dataset["test"].select(range(100))
        results = generator.run(dataset = eval_ds)

        em = EmbeddingModelWrapper()
        similarities = em.get_similarities(
            em.get_embeddings( [a["answer_ref"] for a in results] ),
            em.get_embeddings( [a["answer_pred"] for a in results] ),
        )
        cosine_sim = mean(similarities)
        trainer.log({"cosine_sim": cosine_sim})
trainer.add_callback(SemscoreEvalCallback())

我们观察到,尽管损失曲线发散,但生成答案与参考答案的语义相似度实际上增加到第 7 个 epoch。

image/png

在上图中,对于这个 TinyLlama 训练运行,绝对数字的改进相当微不足道。让我们将这些数字放入更广阔的视角,在相同数据上训练 Mistral(一个更好的基础模型),看看会发生什么。

image/png

请注意,由于适配器的额外开销,使用 LoRA 或 QLoRA 进行训练时推理速度非常慢。在 Mistral 的 QLoRA 训练过程中,对 100 个样本生成最多 500 个令牌的评估大约需要 30 分钟(批量推理)。根据你的担忧程度,最好在训练和合并之后再计算 SemScore,这会快得多。

这些曲线具体是什么样子,取决于模型、数据集和训练超参数。例如,本例中的超参数可能不理想。有人可能还会争辩说学习率太低,数据集太小,或者训练 10 个 epoch 过多。而且,这种比较有点像“橘子比苹果”,因为我们正在比较一个完全微调(所有参数都经过训练)和一个 QLoRA 训练(约 4% 的参数经过训练)。但这在某种程度上也说明了问题:SemScore 提供了另一种快速评估您的模型是否有效地学习了您想要它学习的内容的方式,并允许您探索不同训练方法和超参数的影响。

如果您对 TinyLlama 和 Mistral 训练每个 epoch 生成的答案和参考答案感到好奇,可以在这里找到。重现上述所有内容的完整代码都在笔记本中提供:在训练 TinyLlamaMistral 时计算 SemScore。

总结

  • 评估 LLM 是一项具有挑战性的任务,当前基准存在局限性
  • 人工评估是黄金标准,但扩展性差
  • 其他评估方法,如使用预定义指标或依赖其他 LLM,可能不可靠、有偏见或成本高昂
  • SemScore 是一种评估 LLM 输出语义含义的新方法,提供了模型性能的不同视角。
  • 它不是一个一劳永逸的解决方案,不适用于所有类型的数据集(代码、数学?),而是一种补充方法

这篇博客文章的目的是展示 SemScore 的实用性,并提供如何将其应用于您自己的数据集和模型的实用指导。

我希望这篇帖子对您有所帮助,并鼓励您在自己的模型和训练运行中尝试 SemScore。如果您有任何问题或反馈,请告诉我!

社区

注册登录 以发表评论