概述
在本次项目案例中,需要开发一个由 ML 驱动的 NLP 模型,以准确预测多项选择题中误解和错误答案(干扰项)之间的亲和力。此解决方案将为干扰项建议候选误解,使专业人类教师更容易标记具有误解的干扰项
描述
诊断题是一道多项选择题,有四个选项:一个正确答案和三个干扰项(错误答案)。每个干扰项都经过精心设计,以捕捉特定的误解。例如:
如果学生选择干扰项 “13”,他们可能会产生 “无论优先级顺序如何,都从左到右执行操作”的错误观念。
用适当的误解标记干扰物是必不可少的,但很耗时,并且很难在多个人工贴标机之间保持一致性。误解在描述粒度方面差异很大,当人工贴标机在新的主题领域标记干扰因素时,通常会发现新的误解。
最初尝试使用预先训练的语言模型并未成功,这可能是由于问题中数学内容的复杂性。因此,需要一种更高效、更一致的方法来简化标记过程并提高整体质量。
开发一个由机器学习 (ML) 驱动的自然语言处理 (NLP) 模型,该模型可以预测误解和干扰因素之间的亲和力。目标是创建一个模型,该模型不仅与已知的误解保持一致,而且可以推广到新的、新出现的误解。这样的模型将帮助人工贴标机从现有和新识别的选项中准确地选择合适的误解。
数据集描述
在 Eedi 上,学生回答诊断问题 (DQ),这是多项选择题,具有一个正确答案和三个错误答案,称为干扰项。每个问题都针对一个特定的结构(也称为技能),代表与问题相关的最精细的知识级别。每个干扰因素都旨在与潜在的误解相对应。下面是 DQ 的示例:
在此示例中,问题的选项标有误解,如下所示:
A - 正确(无干扰项)
B - 从左到右执行操作,而不考虑优先级顺序
C - 混淆了运算顺序,认为加法先于除法
D - 混淆运算顺序,认为加法先于乘法
诊断问题最初以图像格式呈现,文本(包括数学内容)是使用人机协同 OCR 流程提取的。
文件和字段信息
[训练/测试].csv
QuestionId- 唯一问题标识符 ()。int
ConstructId- 唯一构造标识符 () 。int
ConstructName- 与问题 () 相关的最精细的知识级别。str
CorrectAnswer- A、B、C 或 D ()。char
SubjectId- 唯一主题标识符 ()。int
SubjectName- 比 () 更通用的上下文。constructstr
QuestionText- 使用人机协同 OCR 从问题图像中提取的问题文本 () 。str
Answer[A/B/C/D]Text- 答案选项 使用人机协同 OCR 从问题图像中提取的文本 ()。str
Misconception[A/B/C/D]Id- 唯一的误解标识符 ()
misconception_mapping.csv
-映射到其MisconceptionIdMisconceptionName
sample_submission.csv - 格式正确的提交文件。
QuestionId_Answer- 每个问题都有 3 个错误答案,需要预测 MisconceptionId
MisconceptionId- 最多可以预测 25 个值,以空格分隔。
完整代码
'''
导入必要的库:
numpy: 用于进行高效的数值计算。
pandas: 数据分析和操作库,用于处理数据框。
torch: PyTorch框架,用于深度学习模型的创建与训练。
transformers: Hugging Face库,主要用于加载预训练的Transformer模型和tokenizer。
cosine_similarity: 用于计算余弦相似度的函数。
re: 正则表达式库,用于文本处理。
cdist: SciPy库中的函数,用于计算不同数据点之间的距离。
kagglehub: Kaggle的API库,用于下载Kaggle上的模型和数据集。
'''
import numpy as np
import pandas as pd
import torch
from transformers import AutoTokenizer, AutoModel
from sklearn.metrics.pairwise import cosine_similarity
import re
from scipy.spatial.distance import cdist
import kagglehub
'''
下载模型:
使用kagglehub下载指定的Transformer模型并将其路径存储在path变量中。
打印模型文件的路径。
'''
path = kagglehub.model_download("happenmass/bge-small-en-v1.5/transformers/bge")
print("Path to model files:", path)
'''
数据加载:
使用pandas读取CSV文件,加载训练集、测试集和误解映射的数据。
'''
# 加载训练和测试数据
train = pd.read_csv("eedi-mining-misconceptions-in-mathematics/train.csv")
test = pd.read_csv("eedi-mining-misconceptions-in-mathematics/test.csv")
misconception_mapping = pd.read_csv("eedi-mining-misconceptions-in-mathematics/misconception_mapping.csv")
'''
模型和设备初始化:
检查CUDA是否可用,设置计算设备为GPU或CPU。
使用transformers库加载预训练的tokenizer和模型,并将模型移动到指定的设备上。
'''
device = "cuda:0" if torch.cuda.is_available() else "cpu"
tokenizer = AutoTokenizer.from_pretrained('bge-small-en-v1.5/transformers/bge/2')
model = AutoModel.from_pretrained('/bge-small-en-v1.5/transformers/bge/2')
model.to(device)
'''
生成文本嵌入:
定义函数generate_embeddings,该函数将输入文本转换为嵌入向量。
按批次处理文本,使用tokenizer进行编码,传入模型得到最后隐藏状态。
从隐藏状态中提取CLS标记的向量,并进行归一化操作,最后返回所有嵌入的合并结果。
'''
def generate_embeddings(texts, model, tokenizer, device, batch_size=8):
all_embeddings = []
for i in range(0, len(texts), batch_size):
batch_texts = texts[i:i + batch_size]
inputs = tokenizer(batch_texts, padding=True, truncation=True, return_tensors="pt", max_length=1024).to(device)
with torch.no_grad():
outputs = model(**inputs)
embeddings = outputs.last_hidden_state[:, 0, :] # CLS token
embeddings = torch.nn.functional.normalize(embeddings, p=2, dim=1)
all_embeddings.append(embeddings.cpu().numpy())
return np.concatenate(all_embeddings, axis=0)
'''
生成误解文本的嵌入:
从误解映射数据中提取误解名称并生成它们的嵌入表示,存储在all_ctx_vector中。
'''
MisconceptionName = list(misconception_mapping['MisconceptionName'].values)
all_ctx_vector = generate_embeddings(MisconceptionName, model, tokenizer, device)
'''
文本预处理:
定义函数preprocess_text,对输入文本进行小写转换、去除非字母字符及多余空格的清洗。
准备测试数据
'''
def preprocess_text(text):
# 转换为小写
text = text.lower()
# 移除特殊字符和数字
text = re.sub(r'[^a-zA-Z\s]', '', text)
# 移除多余空格
text = ' '.join(text.split())
return text
'''
构建完整问题文本:
定义函数make_all_question_text,将构造名称和问题文本合并为一个新列all_question_text,并进行清洗。
'''
def make_all_question_text(df: pd.DataFrame) -> pd.DataFrame:
df["all_question_text"] = df["ConstructName"] + " " + df["QuestionText"]
df["all_question_text"] = df["all_question_text"].apply(preprocess_text)
return df
'''
将数据从宽格式转换为长格式:
定义函数wide_to_long,使用pd.melt将数据从宽格式转换为长格式,以便处理多个答案的文本。
'''
def wide_to_long(df: pd.DataFrame) -> pd.DataFrame:
df = pd.melt(
df[
[
"QuestionId",
"all_question_text",
"CorrectAnswer",
"AnswerAText",
"AnswerBText",
"AnswerCText",
"AnswerDText"
]
],
id_vars=["QuestionId", "all_question_text", "CorrectAnswer"],
var_name='Answer',
value_name='value'
)
return df
''''
定义函数make_all_text,整合问题文本和答案文本到一个新列all_text中。
'''
def make_all_text(df: pd.DataFrame) -> pd.DataFrame:
text_components = []
if "all_question_text" in df.columns:
text_components.append(df["all_question_text"])
if "value" in df.columns:
text_components.append(df["value"].apply(preprocess_text))
df["all_text"] = pd.concat(text_components, axis=1).apply(lambda x: ' '.join(x.dropna().astype(str)), axis=1)
return df
'''
按照问题ID和答案排序test_long数据框,并重置索引。
'''
test_long = test_long.sort_values(["QuestionId", "Answer"]).reset_index(drop=True)
'''
对测试数据中的文本生成嵌入向量,存储在all_text_vector中。
'''
test_texts = list(test_long['all_text'].values)
all_text_vector = generate_embeddings(test_texts, model, tokenizer, device)
'''
使用cosine_similarity计算测试文本嵌入与误解嵌入之间的余弦相似度。
'''
cosine_similarities = cosine_similarity(all_text_vector, all_ctx_vector)
'''
使用cdist计算欧氏距离,并将其转换为相似度。
'''
euclidean_distances = cdist(all_text_vector, all_ctx_vector, metric='euclidean')
euclidean_similarities = 1 / (1 + euclidean_distances) # 将距离转换为相似度
'''
将计算得出的余弦相似度和欧氏相似度取平均,以得到综合相似度。
'''
combined_similarities = (cosine_similarities + euclidean_similarities) / 2
'''
对综合相似度进行排序,并获取排序后的索引。
'''
test_sorted_indices = np.argsort(-combined_similarities, axis=1)
'''
从答案列中提取字母,并生成符合提交格式的新列,然后过滤掉正确答案,最后生成提交的数据框。
'''
# 准备提交
test_long["Answer_alphabet"] = test_long["Answer"].str.extract(r'Answer([A-Z])Text$')
test_long["QuestionId_Answer"] = test_long["QuestionId"].astype("str") + "_" + test_long["Answer_alphabet"]
test_long["MisconceptionId"] = test_sorted_indices[:, :25].tolist()
test_long["MisconceptionId"] = test_long["MisconceptionId"].apply(lambda x: ' '.join(map(str, x)))
# 过滤正确的行
test_long = test_long[test_long["CorrectAnswer"] != test_long["Answer_alphabet"]]
submission = test_long[["QuestionId_Answer", "MisconceptionId"]].reset_index(drop=True)
最后的最后
本次项目案例的主要思路是利用深度学习模型和相似度计算技术,从给定的数学问题和相关的误解信息中提取特征,进而对学生的回答进行误解识别和预测。实现的目标是生成对每个问题的潜在误解的预测结果,以便于进一步分析学生的思维过程和教学效果。
实现的目标
数据加载与预处理:
从CSV文件中加载训练集、测试集和误解映射表,并进行基本的文本清洗和整合以便于后续处理。
模型初始化:
使用Pytorch和Hugging Face的Transformers库加载预训练的深度学习模型(如BGE)和tokenizer,将模型转移到可以利用的计算设备(GPU或CPU)上。
文本嵌入生成:
定义函数生成文本的嵌入向量,提取文本特征,通过批处理加速计算过程。
相似度计算:
计算测试数据的文本嵌入与误解文本嵌入之间的相似度,包括余弦相似度和欧氏距离,将两者结合以获得综合相似度。
结果排序与过滤:
根据综合相似度对误解进行排序,提取对应的错误识别,并格式化为符合提交要求的结果。
准备提交数据:
生成提交格式的数据框,并确保剔除正确的答案。最终数据框仅包含预测的误解ID和对应问题的标识。
作用总结
项目案例核心作用是利用预训练的深度学习模型对用户的回答进行分析,并尝试识别出与之相关的潜在误解。实现了从数据预处理、文本嵌入生成、相似度计算到结果整理的全流程方法,便于教育研究者分析学生的理解过程,以帮助改进教学策略和内容。