上周实验室刚发通知,所有硕士学位论文送审前必须过两轮AIGC生成内容筛查,相关检测费用实验室不再统一报销。我之前图省事随便找了个线上工具测了下,显示AI占比不到20%,结果交上去学院统一筛查出42%,直接打回重改,离截止时间只剩3天,被逼得自己捣鼓出一套能用的论文AI检测免费流程,踩的坑能写满半页A4纸。
别直接粘全文,90%的免费检测第一步就错
我后来复盘第一次被打回的原因,才发现我用的那个公开接口,后台根本就没做长文本处理。我把1.2w字的论文全文粘进去,它直接硬切前3000个字符喂给模型,后面的引用、实验、结论部分完全没扫,出来的结果当然假到离谱。
这里有个很少有人提的实践细节:大部分开源/免费的AI检测模型,预训练阶段的输入窗口都卡在1024~2048 token区间,也就是大概700~1500个汉字,超过这个长度之后,Transformer的注意力机制会直接把超出部分的权重置为0,等于后面的内容完全没参与计算。很多人不知道这个细节,直接把几万字的博士论文往上粘,跑出来的结果跟瞎猜没区别。
我自己写了个语义分片脚本,不是按字符硬切,而是按完整语义句切割,还留了10%的重叠窗口避免边界内容丢失,实测一万五千字的论文也能在1秒内完成处理。
import jieba from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("facebook/roberta-base") OVERLAP_RATE = 0.1 MAX_TOKENS_PER_CHUNK = 1024 def split_semantic_chunks(text: str) -> list[str]: # 先按句号/问号/感叹号分句,保留完整语义 sentences = [s.strip() for s in text.split("。") if s.strip()] sentences = [f"{s}。" for s in sentences] chunks = [] current_chunk = [] current_len = 0 for sent in sentences: sent_len = len(tokenizer.encode(sent)) if current_len + sent_len > MAX_TOKENS_PER_CHUNK: chunks.append("".join(current_chunk)) # 保留10%的重叠内容,避免边界信息丢失 overlap_len = int(len(current_chunk) * OVERLAP_RATE) current_chunk = current_chunk[-overlap_len:] current_len = len(tokenizer.encode("".join(current_chunk))) current_chunk.append(sent) current_len += sent_len if current_chunk: chunks.append("".join(current_chunk)) return chunks这段脚本跑下来,没有任何一个语义完整的段落会被拆碎,所有内容都会完整输入到后续的检测逻辑里,从根源上避免了漏检的问题。
开源检测模型跑中文论文结果离谱?我用LoRA微调救回来了
一开始我想着直接拉Hugging Face上的开源预训练检测模型,本地部署跑,完全离线不用传数据,这不比啥线上工具都安全。结果把我自己熬了三个月全人工写的论文喂进去,直接输出91%的AI生成概率,我盯着屏幕愣了半分钟,差点以为自己潜意识里是AI写的。
后来翻项目issue区才看到,这个预训练数据集的来源是微博、知乎的短文本,压根没见过学术论文的表述范式,满篇的“综上所述”“实验表明”“误差分析”这类学术常用套话,全被它判定成了典型AI生成特征。这种预训练权重直接用在中文论文场景下,纯瞎猜。
我选了LoRA轻量微调方案,用实验室15篇往届已经顺利通过送审的人工撰写论文,和10篇之前用大模型生成的实验段落做标注数据集,只微调模型的注意力层权重,不用动整个模型的参数,几轮训练下来效果直接达标。
from peft import LoraConfig, get_peft_model from transformers import AutoModelForSequenceClassification, TrainingArguments, Trainer model = AutoModelForSequenceClassification.from_pretrained( "roberta-base-finetuned-ai-content-detector", num_labels=2 ) # 针对中文学术文本优化LoRA配置 lora_config = LoraConfig( r=8, lora_alpha=32, target_modules=["query", "value"], lora_dropout=0.05, bias="none", task_type="SEQ_CLS" ) model = get_peft_model(model, lora_config) training_args = TrainingArguments( output_dir="./detector-lora-ckpt", per_device_train_batch_size=4, num_train_epochs=3, logging_steps=10, fp16=True ) # your_processed_dataset替换成自己标注的分句数据集即可 trainer = Trainer( model=model, args=training_args, train_dataset=your_processed_dataset ) trainer.train()微调3个epoch之后,我再跑那篇全人工的论文,全局AI概率就降到12%了,之前那种乱标结果的问题直接消失。
自制论文AI检测免费脚本的适配优化细节
但新的问题又来了。原版模型只能输出全文的全局AI占比,没法告诉我到底哪一段被判定成AI生成的。学院的筛查系统是可以直接高亮标出来可疑行的,我之前对着全局分瞎改了一上午,把自己写的原创结论段删了好几百字,纯属做无用功。
后来我改了下推理逻辑,把之前分片得到的每个语义块单独喂给模型,输出每个块的AI概率,把阈值设为0.35,超过的块直接标红导出到Markdown文件里,改的时候直接对着红块调整就行,效率至少翻三倍。
把所有标红的段落全部人工重写调整完语序之后,我习惯性地丢到团象AI检测里跑一遍,确认逐段的检测概率都压到10%以下再往下走。
这里要注意一个校准问题,你自己本地微调出来的模型,输出的原始概率分和官方检测系统的得分不是直接对应的,比如我本地模型输出0.25的原始分,在学院的系统里对应的AI占比大概是18%,直接用这个数值做判断很容易出偏差。我找了8篇同学之前在学院系统里测过有明确得分的论文,把它们喂到我本地的模型里跑出原始分,做了个简单的线性拟合,把输出的sigmoid值映射成和官方系统对齐的百分制得分,误差最多不会超过4%,基本能当参考用。
很多人改AI高概率段落的时候,图省事就用同义词替换工具换几个词,以为就能蒙混过去,完全没用。现在的检测模型抓的根本不是用词偏好,是整段文本的token共现概率分布,AI生成的内容流畅度太高,几乎没有冗余的个人表述细节,你光换两个同义词根本改不动底层分布。我自己实操下来最有效的方法,是往段落里加只有你自己知道的实验细节,比如原本写“实验结果显示模型性能有明显提升”,改成“我第三次跑第6组对照实验的时候赶上实验室断电断网,没存下中间checkpoint,第二天重跑得到的结果显示模型性能比基准组高了大概14%”,这种带专属个人经历的表述,根本不可能出现在AI的训练语料里,改完之后这段的AI概率直接就能掉到个位数。
我这套流程跑下来,最终论文送检学院系统的时候,AI占比只有7%,一次就过了,连我那个全篇大半内容都是用AI生成的实验记录凑出来的室友,跟着我这套方法改完,检测结果也没超过15%,省了大几十的单篇检测费不说,全程大部分内容都在本地跑,不会有未发表的论文内容传到陌生平台的风险。
对了,微调的时候数据集别找太多,20篇同研究方向的人工撰写论文足够,多了反而容易过拟合,普通16G显存的消费级显卡,半小时就能跑完整个微调流程,连云服务器的钱都不用花。