比赛链接:LLM - Detect AI Generated Text | Kaggle
高分方案作者:SecretSauceArtRidge | Kaggle
高分方案源码:ModelsXRunV1DeepLearning | Kaggle
前言
作为一名研一学生,本着积累经验的原则,我参加了这次内容为《LLM - Detect AI Generated Text》的 Kaggle 竞赛。比赛结束后,我学习了排名前几位的选手给出的方案,并在此写下自己对一篇高分竞赛方案的学习报告,我挑选了一份人气最高的高分方案(源码和作者在本文最上方),梳理了其完成整个比赛的步骤,并且学习和总结了排名靠前的高分解决方案的“秘诀”。
一、实现步骤
1. 数据集处理
1.1 选择数据集
作者首先选择了以下数据集:
(1)DAIGT V2 Train Dataset
作者 Darek Kłeczek | Grandmaster | Kaggle
数据来源:
- 使用 ChatGPT 和 GPT4 生成的文本
- 用 Llama-70b 和 Falcon180b 生成的文本
- 某作家的 2000 篇克劳德散文
- LLM 使用 PaLM 从 Google Gen AI 生成的文章
- 官方训练随笔
- 各种 LLM 论文摘写
(2)LLM: Mistral-7B Instruct texts
作者 Carl McBride Ellis | Grandmaster | Kaggle ,该数据集由Mistral-7B模型生成的4900个文本组成。
(3)LLM Extra dataset
由本实验作者自己创建的私有数据集,使用与(2)相同的方式从Mistral-7B模型生成。
(4)Gemini Pro LLM - DAIGT
由本实验作者自己创建的公共数据集(暂未给出数据来源)。
(5)竞赛数据集
Kaggle 官方给的数据集。
1.2 合并数据集
作者合并以上5个数据集,并保留了15个提示( prompt_name ),最终分为三个数据集。 分别为训练数据集(包含所有数据)、LLM数据集(仅 Mistral 数据与 Gemini 数据--此处所有数据均标记为1,表示文本由ai生成)、竞赛数据集(官方数据集--99%的人标记为0,表示文本由人类所写),命名为 train_old。
def read_train_all():
train = pd.read_csv("/kaggle/input/daigt-v2-train-dataset/train_v2_drcat_02.csv")
train = train[['text', 'prompt_name', 'label']]
train = standardize_categories(train)
train_old = pd.read_csv("/kaggle/input/llm-detect-ai-generated-text/train_essays.csv")
train_old.rename(columns={'generated': 'label'}, inplace=True)
train_old['prompt_name'] = train_old.apply(assign_category, axis=1)
train_old = train_old[['text', 'prompt_name', 'label']]
lm_7b = pd.read_csv("/kaggle/input/llm-mistral-7b-instruct-texts/Mistral7B_CME_v7.csv")
lm_ali_1 = pd.read_csv("/kaggle/input/llm-dataset/gen_llm_fac_v1.csv")
#lm_ali_2 = pd.read_csv("/kaggle/input/llm-dataset/gen_llm_elec_v1.csv")
#lm_ali_3 = pd.read_csv("/kaggle/input/llm-dataset/gen_llm_car_free_v1.csv")
lm_ali_4 = pd.read_csv("/kaggle/input/llm-dataset/gen_llm_exploring_venus_v1.csv")
lm_ali_5 = pd.read_csv("/kaggle/input/llm-dataset/gen_llm_face_on_mars_v1.csv")
lm_ali_6 = pd.read_csv("/kaggle/input/llm-dataset/gen_llm_driveless_cars_v1.csv")
lm_ali_7 = pd.read_csv("/kaggle/input/llm-dataset/gen_llm_cowboy_v1.csv")
lm_ali_8 = pd.read_csv("/kaggle/input/llm-dataset/gen_llm_cowboy_v2.csv")
#lm_ali_9 = pd.read_csv("/kaggle/input/llm-dataset/gen_llm_face_on_mars_v2.csv")
gemini = pd.read_csv("/kaggle/input/gemini-pro-llm-daigt/gemini_pro_llm_text.csv")
gemini = gemini[gemini['typos']=="no"]
#lm_data = pd.concat([lm_7b, lm_ali_1, lm_ali_2, lm_ali_3,lm_ali_4,lm_ali_5,lm_ali_6,lm_ali_7,lm_ali_8,lm_ali_9,gemini], ignore_index=True)
lm_data = pd.concat([lm_7b, lm_ali_1, lm_ali_4,lm_ali_5,lm_ali_6,lm_ali_7,lm_ali_8,lm_ali_8,gemini], ignore_index=True)
lm_data.rename(columns={'generated': 'label'}, inplace=True)
del gemini
gc.collect()
lm_data = lm_data[['text', 'prompt_name', 'label']]
lm_data = standardize_categories(lm_data)
train_old = standardize_categories(train_old)
train = pd.concat([train, lm_data, train_old])
return train, lm_data, train_old
2. 特征选择与提取
2.1 对
prompt_name 分类
作者使用 Tf-idf 向量化器捕捉文本中的关键词信息,并训练了一个逻辑回归模型来预测每个测试文本的
prompt_name(
即任务提示或主题),通过将 TF-IDF 向量化器和逻辑回归模型合并,封装到一个方法里面,使整个流程变得更加整洁和可维护。首先作者训练逻辑回归模型预测测试集中每个文本的 prompt_name ,再计算唯一 prompt_id 的数量,并选择前N个重复次数最多的 prompt_names(其中N是唯一prompt_id的数量),以达到提炼测试集的效果。
对于 prompt_name 的分类原本是一个简单的任务,但作者却训练逻辑回归模型来进行分类,提高数据的准确性的同时得到了可以预测任何数据集 prompt_name 的模型。
def train_for_prompt_names():
train_for_cat, lm_data, train_old = read_train_all()
tfidf_cat = TfidfVectorizer()
X_cat = tfidf_cat.fit_transform(train_for_cat['text'])
X_train_cat, X_test_cat, y_train_cat, y_test_cat = train_test_split(X_cat, train_for_cat['prompt_name'], test_size=0.05, random_state=42)
model_cat = LogisticRegression(max_iter=1000)
model_cat.fit(X_train_cat, y_train_cat)
return tfidf_cat, model_cat
def get_test_prompt_names(test,tfidf_cat,model_cat):
unique_prompt_ids_count = test['prompt_id'].nunique()
print("There are : ", unique_prompt_ids_count, " prompts in the test data")
predefined_prompt_names_list = ['Facial action coding system', 'Driverless cars', 'Exploring Venus', 'The Face on Mars', 'A Cowboy Who Rode the Waves']
if len(test) == 3:
print("select predefined prompts .. ")
return predefined_prompt_names_list
# predict test cats category
test_cats = tfidf_cat.transform(test['text'])
test_prompt_names = model_cat.predict(test_cats)
test['prompt_name'] = test_prompt_names
# get top n
prompt_counts = test['prompt_name'].value_counts()
top_n_prompts = prompt_counts.head(unique_prompt_ids_count).index.tolist()
return top_n_prompts
其他的分类方式:
- 决策树:一种基于树结构的分类方法,它通过不断地对特征进行划分,直到达到某个终止条件为止。决策树的优点是易于理解和解释,缺点是容易过拟合和不稳定。
- 随机森林:一种基于集成学习的分类方法,通过构建多个决策树,并对它们的结果进行投票或平均,来提高分类的准确性和稳定性。其优点是可以处理高维和非线性的数据,缺点是训练和预测的速度较慢,难以解释。
- 支持向量机:一种基于间隔最大化的分类方法,它通过寻找一个超平面来将不同类别的数据分开。其优点是具有良好的泛化能力,缺点是对参数和核函数的选择敏感,计算复杂度较高。
2.2 筛选文本
根据已确定的 prompt_name,从Train数据集中筛选相应的文本。只保留和测试数据集中的 prompt_name 相同或相似的文本,剔除其他不相关的文本。这样做可以减少训练数据的规模,节省训练时间和资源,还能提高对特定 prompt_name 的文本的分类的效果。
def filter_dataframe(df, category):
# Filter the DataFrame for the specified category or NaN in 'prompt_name'
filtered_df = df[(df['prompt_name'] == category) | (df['prompt_name'].isna())]
return filtered_df
def filter_dataframe_single_category(df, category):
# Filter the DataFrame for the specified category in 'prompt_name'
filtered_df = df[df['prompt_name'] == category]
return filtered_df
def standardize_categories(df):
# Standardize the category name
df['prompt_name'] = df['prompt_name'].str.replace('"A Cowboy Who Rode the Waves"', 'A Cowboy Who Rode the Waves', regex=False)
return df
def assign_category(row):
if row['prompt_id'] == 1:
return "Does the electoral college work?"
elif row['prompt_id'] == 0:
return "Car-free cities"
else:
return None # or some default value
2.3 改正错误单词
更正训练集和测试集中拼写错误的单词可以提高机器学习过程中的训练准确性,作者使用了Levenshtein 编辑距离算法来更正单词的拼写,导入如下 python 库即可调用编辑距离函数( lev_search.find_dist() ),lev_search.find_dist() 可用来根据编辑距离改正数据集中的拼写错误,并返回更正单词的具体操作。作者结合正则表达式和编辑距离有效的更正了错误单词。
Levenshtein 编辑距离算法的具体使用方法:
LLM - Detect AI Generated Text | KaggleIdentify which essay was written by a large language modelhttps://www.kaggle.com/competitions/llm-detect-ai-generated-text/discussion/468767
from leven_search import LevenSearch, EditCost, EditCostConfig, GranularEditCostConfig
#spell checker code
def sentence_correcter(text):
wrong_words = []
correct_words = dict()
word_list = re.findall(r'\b\w+\b|[.,\s]', text)
for t in word_list:
correct_word = t
if len(t)>2:
result = lev_search.find_dist(t, max_distance=0)
result = list(result.__dict__['words'].values())
if len(result) == 0:
result = lev_search.find_dist(t, max_distance=1)
result = list(result.__dict__['words'].values())
if len(result):
correct_word = result[0].word
wrong_words.append((t, result))
correct_words[t] = correct_word
dict_freq = defaultdict(lambda :0)
for wrong_word in wrong_words:
_, result = wrong_word
for res in result:
updates = res.updates
parts = str(updates[0]).split(" -> ")
if len(parts) == 2:
from_char = parts[0]
to_char = parts[1]
dict_freq[(from_char, to_char)] += 1
if len(dict_freq):
max_key = max(dict_freq, key=dict_freq.get)
count = dict_freq[max_key]
else:
count = 0
if count > 0.06*len(text.split()):
gec = GranularEditCostConfig(default_cost=10, edit_costs=[EditCost(max_key[0], max_key[1], 1)])
for wrong_word in wrong_words:
word, _ = wrong_word
result = lev_search.find_dist(word, max_distance=9, edit_cost_config=gec)
result = list(result.__dict__['words'].values())
if len(result):
correct_words[word] = result[0].word
else:
correct_word = word
correct_sentence = []
for t in word_list:
correct_sentence.append(correct_words[t])
return "".join(correct_sentence)
2.4 文本数据标记化
为了进行文本数据的标记化( tokenization ),将文本分割成有意义的单元,作者使用 BPE 算法训练标记器,以数据作为输入,从输入数据的文本列创建新的数据集,并使用具有指定词汇表大小和特殊标记的 BPE 训练器对标记器进行训练。使用的分词表大小为5000,作者创建了3个标记化数据集,Train、test 和 test+llm_data,用于将模型拟合到这些数据集。
class BPETokenizer:
ST = ["[UNK]", "[PAD]", "[CLS]", "[SEP]", "[MASK]"]
def __init__(
self,
vocab_size,
):
self.vocab_size = vocab_size
self.tok = Tokenizer(models.BPE(unk_token="[UNK]"))
self.tok.normalizer = normalizers.Sequence([normalizers.NFC()])
self.tok.pre_tokenizer = pre_tokenizers.ByteLevel()
@classmethod
def chunk_dataset(cls, dataset, chunk_size=1_000):
for i in range(0, len(dataset), chunk_size):
yield dataset[i : i + chunk_size]["text"]
def train(self, data):
trainer = trainers.BpeTrainer(vocab_size=self.vocab_size, special_tokens=self.ST)
dataset = Dataset.from_pandas(data[["text"]])
self.tok.train_from_iterator(self.chunk_dataset(dataset), trainer=trainer)
return self
def tokenize(self, data):
tokenized_texts = []
for text in tqdm(data['text'].tolist()):
tokenized_texts.append(self.tok.encode(text))
return tokenized_texts
def train_tokenizer(train, lm_data, train_old, test):
tokenizer_train_data = pd.concat([lm_data,train_old])
tok_data = pd.concat([ tokenizer_train_data[["text"]], test[["text"]] ]).reset_index(drop=True)
vc_counters = {}
for vs in [5_000]: # for if needed to train multi vocab_size
bpe_tok = BPETokenizer(vs).train(tok_data)
ctr = Counter(chain(*[x.ids for x in bpe_tok.tokenize(tok_data)]))
vc_counters[vs] = (bpe_tok, ctr)
tqdm.write(f"completed tokenization with {vs:,} vocab size")
return vc_counters
def tokenize_datasets (vc_counters, train, lm_data, test):
bpe_tok = vc_counters[5_000][0]
test_extend = pd.concat([lm_data,test])
tokenized_texts_train = [x.tokens for x in bpe_tok.tokenize(train)]
tokenized_texts_test = [x.tokens for x in bpe_tok.tokenize(test)]
tokenized_texts_lm_data = [x.tokens for x in bpe_tok.tokenize(lm_data)]
tokenized_texts_test2 = tokenized_texts_lm_data + tokenized_texts_test
del tokenized_texts_lm_data
gc.collect()
return tokenized_texts_train, tokenized_texts_test, tokenized_texts_test2
常用于文本数据标记化的方法:
- WordPiece:一种基于子词的文本标记化方法,它通过将词分割为最小的有意义的单元来减少词汇表的大小。WordPiece 算法被广泛应用于神经机器翻译和预训练语言模型,如 BERT。
- SentencePiece:一种基于字节的文本标记化方法,它不依赖于空格或标点符号来分割文本,而是直接在原始文本上进行标记化。SentencePiece 算法可以使用不同的标记化策略,如 BPE、unigram 或 char
- Jieba:一种基于词的文本标记化方法,它专门用于中文文本的分词。Jieba 算法使用了基于前缀词典的最大匹配法、隐马尔可夫模型和条件随机场等技术,可以实现高效和准确的中文分词
3. 模型训练和调优
3.1 训练Distilroberta模型
作者定义了 Distilroberta 模型的训练和推断过程,通过调用 Hugging Face 的 Transformers 库提供的
Trainer
类来管理这些任务,同时利用了
AutoTokenizer
的功能,自动匹配适合的模型。
标签预测阶段使用了激活函数Sigmoid,它的梯度相对平滑,有助于更稳定的模型训练,同时确保输出在[0, 1]范围内,避免了预测值的爆炸。
model_checkpoint_base = "/kaggle/input/distilroberta-base/distilroberta-base"
model_checkpoint_infer = "/kaggle/input/detect-llm-models/distilroberta-finetuned_v5/checkpoint-13542"
def train_inference_transformer_runtime(train,train_from_sub,test):
#train = append_train_from_sub_phase(train, train_from_sub)
valid = train_from_sub
test = test[['id', 'text']]
# sk = StratifiedKFold(n_splits=10,shuffle=True,random_state=42)
# train0 = train
# for i, (tr,val) in enumerate(sk.split(train0,train0.label)):
# train = train0.iloc[tr]
# valid = train0.iloc[val]
# break
train.text = train.text.fillna("")
valid.text = valid.text.apply(lambda x: x.strip('\n'))
train.text = train.text.apply(lambda x: x.strip('\n'))
ds_train = Dataset.from_pandas(train)
ds_valid = Dataset.from_pandas(valid)
#tokenizer = AutoTokenizer.from_pretrained(model_checkpoint_base)
ds_train_enc = ds_train.map(preprocess_function_train, batched=True)
ds_valid_enc = ds_valid.map(preprocess_function_train, batched=True)
model = AutoModelForSequenceClassification.from_pretrained(model_checkpoint_base, num_labels=2)
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
early_stopping = EarlyStoppingCallback(early_stopping_patience=5)
num_train_epochs=10.0
metric_name = "roc_auc"
model_name = "distilroberta"
batch_size = 2
args = TrainingArguments(
f"{model_name}-finetuned_v5",
evaluation_strategy = "epoch",
save_strategy = "epoch",
learning_rate=2e-5,
lr_scheduler_type = "cosine",
save_safetensors = False,
optim="adamw_torch",
per_device_train_batch_size=batch_size,
per_device_eval_batch_size=batch_size,
gradient_accumulation_steps=8,
num_train_epochs=num_train_epochs,
weight_decay=0.01,
load_best_model_at_end=True,
metric_for_best_model=metric_name,
save_total_limit=2,
)
trainer = Trainer(
model,
args,
train_dataset=ds_train_enc,
eval_dataset=ds_valid_enc,
tokenizer=tokenizer_train,
callbacks = [early_stopping],
compute_metrics=compute_metrics
)
trainer.train()
trained_model = trainer.model
test_ds = Dataset.from_pandas(test)
test_ds_enc = test_ds.map(preprocess_function_infer, batched=True)
trainer = Trainer(trained_model,tokenizer=tokenizer_train,)
test_preds = trainer.predict(test_ds_enc)
logits = test_preds.predictions
probs = sigmoid(logits)[:,1]
return probs
def get_predictions_tranformer(test):
#model_checkpoint_infer = "/kaggle/input/detect-llm-models/distilroberta-finetuned_v5/checkpoint-13542"
model = AutoModelForSequenceClassification.from_pretrained(model_checkpoint_infer, num_labels=2)
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model.to(device)
test_ds = Dataset.from_pandas(test)
test_ds_enc = test_ds.map(preprocess_function_infer, batched=True)
trainer = Trainer(model,tokenizer=tokenizer_infer,)
test_preds = trainer.predict(test_ds_enc)
logits = test_preds.predictions
final_preds_trans = sigmoid(logits)[:,0]
return final_preds_trans
final_preds_trans_DistilRoberta = get_predictions_tranformer(read_test())
3.2 通过弱监督学习整合预测结果
** ** 作者通过先从整个训练数据中选择与测试数据中相似的文本,以用于弱监督模型的训练。同时使用训练好的 Tokenizer,对训练数据和测试数据进行标记,再将标记后的数据向量化,得到 TF( Term Frequency )矩阵,再对 TF 矩阵进行特征缩放,并使用支持向量回归( Ridge/LinearSVR )模型,基于缩放后的 TF 矩阵对标签进行预测。最后将支持向量回归( Ridge/LinearSVR )模型预测的结果与4.1中 DistilRoberta 模型预测结果进行加权整合,得到最终的标签。
def Train_Linear_With_Infer_WEAK(X_MIDDLE,SIMILAR_ROWS, test_data, only_7_prompts, final_predictions, final_preds_trans, train, lm_data, train_old):
test = test_data
selected_rows = get_selected_rows_from_test(test,X_MIDDLE,final_predictions, final_preds_trans)
similar_texts_df = get_similar_train_text(selected_rows, SIMILAR_ROWS,train, lm_data, train_old)
#train, lm_data, train_old = read_train(only_7_prompts = only_7_prompts)
train = similar_texts_df
train = train.drop_duplicates(subset='text', keep='first')
train = train[['text', 'label']]
train.reset_index(drop=True, inplace=True)
test = selected_rows
print("train tokenizer ... iter: over weak")
vs_counters = train_tokenizer(train, lm_data, train_old, test)
print("tokenize datasets ... iter: over weak")
tokenized_texts_train, tokenized_texts_test, tokenized_texts_test2 = tokenize_datasets (vs_counters, train, lm_data, test)
print("verctorize datasets ...iter: over weak")
tf_train, tf_test = vectorizer_of_data(tokenized_texts_train,tokenized_texts_test,tokenized_texts_test2, 2 ,test)
del tokenized_texts_test2, tokenized_texts_test, tokenized_texts_train, vs_counters
gc.collect()
print("predictions ... iter: over weak")
X_train_scaled, X_test_scaled = MaxAbsScalerTransform(tf_train,tf_test)
preds_linear_weak= get_predictions_linear_LinearSVR(X_train_scaled, X_test_scaled, train['label'].values)
del tf_train, tf_test, X_train_scaled, X_test_scaled
gc.collect()
trans_sel_pred = selected_rows['trans_label']
trans_sel_pred = trans_sel_pred.values
selected_rows_final_pred = 0.5*preds_linear_weak +0.5*trans_sel_pred
selected_rows['label'] = selected_rows_final_pred
test = read_test()
test['label'] = final_predictions
test.set_index('id', inplace=True, drop=True)
selected_rows.set_index('id', inplace=True, drop=True)
# Update test_df with the new label values from selected_rows
test.update(selected_rows)
# Reset index if you want to revert 'id' back to a column
test.reset_index(inplace=True)
return test['label']
3.3 通过强监督学习整合预测结果
与弱监督学习类似,这里作者定义了一个训练和推断的过程,使用线性模型( LinearSVR )的预测结果与之前的模型( Distilroberta )预测结果进行加权组合,**不同之处在于这次进行了多次的迭代优化,定义了
X_TOP
和
Y_BOTTOM
两个参数用于控制每轮迭代中训练数据的划分,同样使用线性模型对迭代数据进行训练和推断,同时在每一轮迭代中使用之前的模型预测结果来构建新的训练数据**。这样的迭代过程有助于逐步改进模型的性能,通过反复使用模型预测结果构建新的训练数据,以便模型更好地捕捉数据的特征。
二、总结
1. 该方案的优势
众所周知,Kaggle 是含金量相当高的竞赛,能在这种竞赛中名列前矛的团队,一定有自己的独到之处,这里我结合作者的分享概括了该方案的优势。
1.1 专注于使用伪标签
作者结合了有标签数据和无标签数据来提高模型性能。在伪标签方法中,模型首先使用有标签数据进行训练,然后利用该模型对无标签数据进行预测。这些预测结果被视为“伪标签”,并与原始有标签数据一起用于重新训练模型,在这种情况下,测试数据来自同一组,因此过度拟合会产生更高的分数。
1.2 巧妙的进行模型集成
也有许多选手使用伪标签来训练模型,但却没有得到很好的效果,这可能是由于他们的模型不够强大,导致预测的标签不准确。作者将传统 NLP 技术与高级语言模型结合起来,集成了新颖且强大的模型,例如使用逻辑回归进行即时分类并结合字节对编码和 Tf-idf,以及战略性地使用 distilroberta 进行集成建模。巧妙的数据处理和模型集成让作者的分数能够名列前茅。
2. Kaggle 比赛的经验
2.1 比赛实施流程
(1)了解、分析问题;
(2)获取训练集和测试集;
(3)进行数据整理和清洗;
(4)分析识别模式并探索数据,进行特征处理;
(5)建模、预测和解决问题
(6)提交结果。
2.2 比赛心得
(1)数据处理
实际竞赛过程中,会遇到许多缺失的数据,考虑列出每个特征的缺失比例,比例过大的直接舍弃,否则想办法填充。舍弃特征会丢掉有用信息,填充会引入噪声,具体怎么操作可以根据模型实际的表现来调整。
(2)特征工程
在Kaggle竞赛中,特征工程是至关重要的。如果能发现甚至创造出对问题关键的特征,可以极大地提升模型性能,使得最终的结果远远超出那些仅使用相同模型却没有进行深度特征工程的参与者。
(3)模型选择
Kaggle 竞赛中如果想获得好的分数,不可能只使用单一模型,一定要考虑将多个模型集成在一起,不管用什么方法融合,想要模型融合之后效果好,模型之间要有多样性。换句话说,模型之间越不相似,模型融合的效果越好。
(4)考虑使用伪标签
通过这种方式改善模型在实际情况下可能会导致过拟合,但是在一些关注结果的比赛却能取得奇效。
版权归原作者 -Qwer 所有, 如有侵权,请联系我们删除。