ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

用Python分析论文摘要风格漂移:从文本指标到LLM辅助写作检测

用Python分析论文摘要风格漂移:从文本指标到LLM辅助写作检测 一项针对 PubMed 生物医学论文的文本分布分析显示2023 年之后大量论文的摘要和正文出现了与大语言模型辅助写作高度一致的文体特征例如句长分布收窄、跨段落过渡词频率上升、原本少见的“突发词”集中出现。这个现象并不只属于“AI 检测”这个细分话题对经常使用 LLM 完成英文润色、文献综述和初稿撰写的开发者来说它同时也是一个很好的文本分析样本如果一篇论文是否由 LLM 辅助过能从词汇密度、句长标准差、过渡短语频率等统计指标中看出端倪那么这些指标就是可以写成代码、跑出结果、并且可以反复验证的工程对象。这篇文章会围绕这套思路带你从零实现一个“论文摘要风格漂移分析工具”。你会看到如何准备语料、如何计算文本指纹指标、如何按年份分组观察变化趋势也会清楚知道哪些指标只能作为参考、哪些信号容易误判。最终产物是一个可复用的 Python 命令行工具适合用于科研文本分析、写作辅助系统质量评估以及期刊初审环节的辅助筛查。1. 先理解这个研究结论背后的检测思路1.1 LLM 辅助写作为什么会在文本里留下痕迹判断一篇文章是否经过 LLM 辅助并不一定需要模型自己来“感觉”。大语言模型在生成英文文本时倾向于选择概率较高的词序列这会导致两个可测量的结果词汇选择变得更加可预测句式结构变得更加均匀。换句话说一个人写论文时会有大量“个人噪声”某个作者偏爱长句、另一个作者习惯使用被动语态、第三个人喜欢用分词开头。而大多数通用大模型经过 RLHF 和指令微调后会收敛到一种“平均风格”这种风格在统计上很容易和人类写作者区分开。于是我们不需要判断“这句话是不是 AI 写的”只需要统计一批文本的词汇多样性、句子长度波动和过渡短语频率就能发现风格漂移。1.2 为什么生物医学论文适合作为分析对象生物医学论文有四个特点让它们特别适合做这类文本分析第一语料公开且结构规范。PubMed 上的摘要格式统一通常包含 Background、Methods、Results、Conclusion 四个部分天然适合做句子级切分。第二写作风格长期稳定。生物医学论文的文体在几十年内相对保守研究者有固定的表达习惯例如被动语态、第三人称、结构化的过渡词。这种稳定性意味着 2023 年之前的历史语料可以作为基线。第三时间标签清晰。每篇论文都有明确的投稿时间、发表时间和接收时间可以按年份或季度精确分组观察风格变化的时间拐点。第四样本量大。PubMed 每年新增论文数量可观即使只抽取摘要子集也能获得足够的统计功效。但要注意这些特点只是让分析更容易不代表结论必然可靠。后面会讲到语料偏差和清洗不一致仍然会严重干扰结果。1.3 常见文本检测指标及其原理学术界和工业界讨论 LLM 辅助写作时通常依赖四类指标。下表概括了它们各自的原理和适用场景。指标类别代表指标检测原理优点局限词汇多样性TTR、MSTTRLLM 生成的文本词汇重复率偏高或偏低计算简单、可解释性强受文本长度影响明显句式波动句长均值、句长标准差人类写作句子长度波动大LLM 输出更均匀容易实现、适合批量比对无法识别逐句改写过渡词频率Moreover、Furthermore、In conclusion 等LLM 在生成结构化文本时过度使用连接词速度快、能解释“模板感”不同学科差异大困惑度Perplexity模型对文本的预测概率越低越可能是人类写作检测能力强依赖特定模型计算成本高本文会让前三种指标作为主选项因为它们在普通笔记本电脑上即可运行困惑度作为进阶指标会在第 6 节单独说明。注意这些指标描述的是“文本分布的集体特征”不是“单篇论文的铁证”。把指标用于单篇判定时要非常谨慎。2. 准备环境、语料和工具链2.1 工具版本和依赖为了让分析结果可复现建议使用稳定版本组合。下面是我在本地验证过的环境工具/库版本建议用途Python3.10 或 3.11主语言pandas2.0 以上数据清洗与分组统计nltk3.8 以上分词、断句和停用词matplotlib3.7 以上趋势可视化scipy1.10 以上统计检验和置信区间安装命令python -m venv .venv source .venv/bin/activate pip install pandas nltk matplotlib scipynltk 需要下载 punkt 分词模型和停用词数据import nltk nltk.download(punkt) nltk.download(stopwords)学习环境里直接用上述命令就能跑通生产环境建议把依赖固定到requirements.txt并单独设置数据目录。2.2 语料来源和预处理策略本工具的输入是一份 CSV 文件至少包含两列year和abstract。如果你要复现“PubMed 论文风格漂移”分析可以按自己的检索条件导出摘要。一个常见的做法是在 PubMed 中按指定关键词检索。导出为 CSV字段包含 Publication Year、Abstract。将数据统一转成下面的格式year,abstract 2019,A randomized controlled trial was conducted to evaluate... 2019,A total of 120 patients were enrolled... 2020,The results indicated that... ... 2024,This study investigates the efficacy of...预处理策略决定了指标的可靠性。至少要做四步小写化统一大小写避免The和the被当成两个词。去除多余空白把换行和连续空格标准化。句子切分使用 nltk 的sent_tokenize它比简单按句号切分更适应英文缩写和引号。可选停用词过滤计算 TTR 时是否过滤停用词会影响结果。建议同时计算原始 TTR 和过滤停用词后的 TTR因为 LLM 在功能词使用上也存在偏差。2.3 分析指标如何落地为代码在设计工具之前先明确每个指标的计算口径TTRtype-token ratio文本中不同词的数量除以总词数。词汇量越小TTR 越低文本越可能重复。MSTTRmoving-average type-token ratio将文本切成固定长度窗口计算每个窗口的 TTR 再取平均减少长度影响。句长均值句子平均包含的单词数。句长标准差句子长度的离散程度。人类写作标准差异常大LLM 辅助文本往往更“平”。过渡短语频率统计 Moreover、Furthermore、Additionally、In conclusion、Overall 等在每 1000 词中的出现次数。这些指标合在一起相当于给每篇摘要打了一个“风格特征向量”。3. 构建一个论文摘要风格漂移分析工具3.1 项目结构和核心模块我建议按下面这样的结构组织代码方便后续扩展llm_writing_analysis/ ├── data/ │ └── abstracts.csv ├── features.py ├── analyze.py ├── report.py └── output/ ├── metrics_by_year.csv └── trend_chart.pngfeatures.py负责把一篇摘要转成指标字典analyze.py负责批量读取 CSV、按年份分组统计report.py负责生成图表和汇总表。3.2 文本清洗和句子切分第一篇文本进入指标计算之前先做清洗。下面代码展示了features.py的基础部分import re import nltk from nltk.tokenize import sent_tokenize, word_tokenize from collections import Counter STOPWORDS set(nltk.corpus.stopwords.words(english)) TRANSITION_WORDS { moreover, furthermore, additionally, in conclusion, overall, however, in addition, therefore, consequently, } def clean_text(text: str) - str: text text.lower() text re.sub(r\s, , text) text re.sub(r[^a-z0-9\s.,;:()\-%], , text) return text.strip() def split_sentences(text: str): return sent_tokenize(text)这段代码把文本小写化并把连字符、百分号等生物医学论文常见字符保留下来。这里要特别注意不要用过于激进的正则把所有非字母字符都删掉否则IL-6、PD-L1、3-day这类关键表达会被拆坏TTR 和句长都会失真。3.3 计算词汇多样性、句长分布和过渡词频率接下来实现指标计算。一个细节是计算 TTR 时先过滤停用词过渡词频率又基于原始文本这两者不要混在一起统计。def compute_ttr(words): if not words: return 0.0 return len(set(words)) / len(words) def compute_msttr(words, window_size: int 50): n len(words) if n window_size: return compute_ttr(words) scores [] for start in range(0, n - window_size 1, window_size): window words[start:start window_size] scores.append(compute_ttr(window)) return sum(scores) / len(scores) def compute_sentence_features(sentences): lengths [len(word_tokenize(s)) for s in sentences] mean_len sum(lengths) / len(lengths) if lengths else 0.0 if len(lengths) 2: variance sum((x - mean_len) ** 2 for x in lengths) / (len(lengths) - 1) std_len variance ** 0.5 else: std_len 0.0 return mean_len, std_len, lengths def compute_transition_ratio(text: str, total_words: int) - float: pattern re.compile(r\b( |.join(map(re.escape, TRANSITION_WORDS)) r)\b) matches pattern.findall(text) return len(matches) / (total_words / 1000) if total_words else 0.0compute_msttr的作用特别重要。整体 TTR 会随着文本变长而下降这是语言学中确定的规律所以直接用整体 TTR 去比较长短不同的摘要是不公平的。MSTTR 每个窗口固定 50 个词可以缓解长度偏差。compute_transition_ratio返回的是每 1000 词中过渡短语的频次单位统一为“每千词”在不同长度摘要之间才能比较。3.4 按时间分组对比变化有了单篇指标后analyze.py负责完成分组和汇总。下面代码展示了核心逻辑import pandas as pd from features import ( clean_text, split_sentences, compute_ttr, compute_msttr, compute_sentence_features, compute_transition_ratio, ) from nltk.tokenize import word_tokenize def process_abstract(abstract: str) - dict: cleaned clean_text(abstract) sentences split_sentences(cleaned) tokens word_tokenize(cleaned) content_tokens [w for w in tokens if w not in STOPWORDS] mean_len, std_len, _ compute_sentence_features(sentences) total_words len(tokens) return { ttr: compute_ttr(content_tokens), msttr: compute_msttr(content_tokens), mean_sent_len: mean_len, std_sent_len: std_len, transition_per_1k: compute_transition_ratio(cleaned, total_words), word_count: total_words, } def analyze(df: pd.DataFrame) - pd.DataFrame: rows [] for _, row in df.iterrows(): features process_abstract(row[abstract]) features[year] row[year] rows.append(features) return pd.DataFrame(rows) if __name__ __main__: input_path data/abstracts.csv df pd.read_csv(input_path) features_df analyze(df) grouped features_df.groupby(year).agg( ttr_mean(ttr, mean), msttr_mean(msttr, mean), mean_sent_len_mean(mean_sent_len, mean), std_sent_len_mean(std_sent_len, mean), transition_per_1k_mean(transition_per_1k, mean), sample_count(year, count), ).reset_index() grouped.to_csv(output/metrics_by_year.csv, indexFalse) print(grouped)这里的关键是先把每一篇摘要转成一条特征记录然后按年份聚类。不要用“把所有年份的文本拼成一个大字符串再统计”否则年份之间无法比较。运行后你会得到类似下面这样一张表yearttr_meanmsttr_meanmean_sent_len_meanstd_sent_len_meantransition_per_1k_meansample_count20190.4830.51223.19.78.2120020200.4860.51522.89.58.6135020210.4810.51022.59.39.0128020220.4790.50922.09.110.1140020230.4720.50120.47.218.7152020240.4690.49619.86.822.31600这张表代表了典型的“风格漂移”2023 年之后句长标准差显著下降过渡词频率明显上升。这就是论文中“显示出 LLM 辅助写作迹象”的统计源头。4. 结果解读怎么判断“是否可能出现 LLM 辅助”4.1 指标变化方向如何解读拿到上面的表格后不能只看一个指标要结合多个方向同时变化。通常可以这样理解如果句长标准差从 9.0 降到 6.8 附近说明句子长度分布变窄。人类作者往往长句短句交替而 LLM 生成的英文倾向于稳定的 15 到 25 词句长。如果过渡词每千词频率从 8 上升到 20 以上说明“Furthermore”“Moreover”“In conclusion”这类连接词被大量使用。这是 LLM 生成学术摘要时最明显的模板痕迹之一。如果整体 TTR 下降但 MSTTR 没有同比例下降说明摘要整体更短或段落结构更机械。MSTTR 仍在相同水平时则说明词汇多样性本身没有骤变不宜过度解读。在解读时请始终记住一个原则这些指标是在描述群体趋势不是在给单篇论文贴标签。如果一篇 2019 年的论文碰巧用词重复、句子均匀它也会落在“疑似 LLM 辅助”区间。这也是为什么研究论文一般说“surge in LLM-associated words”而不是“this paper was written by AI”。4.2 单个摘要评估与批量趋势评估要分开批量趋势评估可以用上述方法但如果你需要对单篇摘要给一个分数必须换成另一套流程并且要引入置信区间和参考分布。操作上可以这样做用 2019 到 2021 年的论文作为基线分布。把单篇摘要的五个指标分别映射到基线分布的百分位。综合百分位得到一个可疑度评分。示例代码import numpy as np def percentile_score(value, baseline_values): return float(np.mean(np.array(baseline_values) value)) baseline_year 2019 baseline_df features_df[features_df[year] baseline_year] def evaluate_single(features: dict, baseline_df: pd.DataFrame) - dict: scores {} for col in [msttr, mean_sent_len, std_sent_len, transition_per_1k]: scores[col] percentile_score(features[col], baseline_df[col]) return scores当transition_per_1k的百分位达到 0.99说明这篇文章的过渡词频率已经高于 99% 的基线论文这确实是一个强信号。但它只是“与基线分布不一致”不等同于“一定是 AI 写的”。注意不要把多个指标的百分位简单平均后当成最终答案。正确做法是分别观察然后对异常指标做交叉验证。4.3 可视化输出折线图和热力图趋势分析最适合用折线图呈现。你可以用 matplotlib 生成一张多子图图片分别展示transition_per_1k_mean、std_sent_len_mean和msttr_mean随时间变化的曲线import pandas as pd import matplotlib.pyplot as plt metrics pd.read_csv(output/metrics_by_year.csv) fig, axes plt.subplots(1, 3, figsize(15, 4)) axes[0].plot(metrics[year], metrics[transition_per_1k_mean], markero) axes[0].set_title(Transition word frequency per 1000 words) axes[0].axvline(2022.5, colorred, linestyle--, alpha0.6) axes[1].plot(metrics[year], metrics[std_sent_len_mean], markero) axes[1].set_title(Std of sentence length) axes[1].axvline(2022.5, colorred, linestyle--, alpha0.6) axes[2].plot(metrics[year], metrics[msttr_mean], markero) axes[2].set_title(MSTTR) axes[2].axvline(2022.5, colorred, linestyle--, alpha0.6) fig.tight_layout() plt.savefig(output/trend_chart.png, dpi160)红虚线可以帮助读者快速定位“风格突变”的时间窗口。对于 2023 年前后出现明显断层的指标可以进一步用 scipy 做统计检验from scipy import stats before features_df[features_df[year] 2021][std_sent_len] after features_df[features_df[year] 2023][std_sent_len] t_stat, p_value stats.ttest_ind(after, before, equal_varFalse) print(ft{t_stat:.3f}, p{p_value:.6f})如果 p 值极小只能说“差异在统计上显著”仍然不能推出因果关系。是否由于 LLM 辅助写作导致需要结合语料来源、作者国别分布、投稿政策变化等信息综合判断。5. 验证与常见问题排查5.1 如何验证分析结果可靠分析结果需要经过至少三重验证否则很容易出现“跑出图来但结论站不住”的情况。第一重验证是人工抽样。从每个年份中随机抽取 20 到 30 篇摘要由人工判断是否存在明显的模板化表达再和指标结果对照。如果人工判断和指标高度一致说明指标方向合理。第二重验证是稳定性验证。把语料随机切分成两份分别计算指标检查两条趋势曲线是否类似。如果第二次分组后趋势消失说明结果可能来自小样本噪声。第三重验证是反向验证。找一批已经被公开讨论“确定包含 LLM 辅助成分”的文章也找一批在 2023 年前发表的经典文献分别计算指标确认工具能把两组区分开。我用一个 1000 条摘要的模拟数据测试过正常结果应该能看到 2023 年前后指标出现明显变化如果年份分组只有两三百条样本折线会非常抖动不建议下任何结论。5.2 常见误判场景和排查路径以下是最常遇到的五类问题每类都给出了现象、原因和解决方式。问题现象常见原因处理建议2019 年摘要过渡词频率就很高语料集中在评论型文章这类文体本身爱用连接词分文体建模不要混合论著和评论清洗后单词大量丢失正则在英文标点和连字符处理上过于激进保留-、%、括号对IL-6做白名单句长标准差异常偏大摘要中包含了试剂、药物名称等超长 token把字符超长的 token 记录为词汇即可不删除2023 年前后样本数量差异太大检索时间窗口不完整或某一年检索条件变化按比例抽样或对每年样本量做下采样TTR 和 MSTTR 趋势矛盾摘要长度分布变化或停用词过滤不一致统一停用词表在报告中同时输出平均词数5.3 数据集偏差和样本量问题PubMed 语料有一个隐蔽的偏差不同国家的作者、不同期刊风格混在一起。如果某一年某本大型期刊大规模投稿来自相同群体的表达习惯会集中出现可能被误判为“LLM 辅助写作潮”。缓解方法是按期刊或来源分层次统计。比如先只分析影响因子相近的 50 本期刊再扩展语料。另一个偏差是时效性2023 年之后的摘要可能更短因为部分期刊调整了摘要字数限制。字数变少会降低整体 TTR这个影响和 LLM 辅助写作的效应混淆。因此在使用整体 TTR 时必须在报告里同时给出平均单词数。如果平均单词数也在下降那么 TTR 下降只能作为次要证据。6. 不同使用场景的实践建议6.1 学术研究者怎么做才能避免过度解读如果你是研究者想把这个分析整理成报告或论文最需要守住的是结论边界。建议使用这样的表述“从 2023 年开始本研究观察的摘要语料在词汇多样性、句长标准差和过渡词频率上出现了显著偏移这一偏移与公开可用的 GPT 系列模型发布时间高度重合但不能排除同期其他因素影响。”同时不要使用任何“检测工具判定某篇文章为 AI 生成”这样的措辞因为任何统计指标在单篇尺度上都有相当的误差。更好的做法是把整批语料放在一起展示集体趋势。6.2 期刊编辑和审稿人如何用这类工具期刊编辑可以把这类分析用在初审环节但只能作为“优先人工复审”的信号而不是拒绝稿件的基础。一个可落地的流程是先对整批新投稿计算指标。如果某篇摘要的transition_per_1k百分位超过 0.95或std_sent_len百分位低于 0.05就标记为“需关注”。将标记稿件转入人工复核由编辑阅读摘要和正文重点查看是否存在空泛连接词和重复句式。人工复核后仍然存疑再与作者沟通文风问题而不是直接判定违规。这套流程可以把误报率控制在一个可接受范围内。对开发者的额外启示是可以把这套规则做成一个简单的内部 Web API编辑上传 PDF 后自动抽取摘要并返回风险分。6.3 技术开发者的可复用清单如果你打算把文本指纹分析嵌入自己的系统下面的检查清单可以直接用于上线前确认。检查项确认内容数据路径CSV 是否包含 year 和 abstract 两列文本是否有空值清洗管道大小写、空白、标点、连字符处理是否符合学科语言习惯句子切分是否用 nltk 或 spaCy 的成熟切分器而不是简单split(.)指标口径每个指标是否按统一词数或窗口归一化基线选择是否选择了 LLM 大规模普及前的时间段作为基线样本量每组样本是否大于 100 篇统计检验是否对前后年份差异做了显著性检验人工抽样是否有人工审阅结果作为对照单篇阈值是否设定了多指标联合规则而不是单指标直接下结论报告字段是否同时输出样本量、平均词数和置信区间如果你需要判断某篇摘要的困惑度可以引入一个较小的生成模型。这里给一个思路性示例用 transformers 加载一个小模型计算句子平均困惑度from transformers import AutoTokenizer, AutoModelForCausalLM model_name distilgpt2 tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForCausalLM.from_pretrained(model_name) def perplexity_of_text(text: str) - float: import torch inputs tokenizer(text, return_tensorspt) with torch.no_grad(): outputs model(**inputs, labelsinputs[input_ids]) loss outputs.loss return float(torch.exp(loss))困惑度的基本直觉是如果模型惊讶程度低困惑度低说明这段文字押中了模型熟悉的概率分布那它由同类模型生成的可能性就大。但要注意困惑度高度依赖所选模型不同模型的排序结果可能不同。因此不要把困惑度数值当成绝对标尺而是当成交叉验证指标。6.4 从“检测”走向“写作辅助系统优化”把视角再放宽一点这些文本指纹指标不只是用来检测 LLM 写作也能用来优化基于 LLM 的科研写作辅助系统。比如系统在润色一段摘要时可以实时计算润色前后句长标准差和过渡词频率一旦发现“过度平滑”或“连接词过密”就自动提示用户调整。这类功能已经超出“判断是否 AI 写作”的范畴本质上是在用文本统计手段衡量生成质量。结合 LLM Agent 或 RAG 流程做论文润色时这个思路可以作为质量评估模块的补充由于论文写作更强调风格稳定加入这类可计算的约束比单纯提示“请自然一点”更可控。从实际项目角度最有价值的下一步是把你自己的领域语料按年份建好基线然后定期分析新论文的风格偏移。这样你能在自己的数据上验证哪些词汇特征最敏感而不是依赖某个公开检测工具的“黑盒分数”。当你积累了自己的基线、清洗管道和评估流程再面对“大多数生物医学出版物是否体现了 LLM 辅助写作”这类问题时就能用代码和数据给出自己的判断而不是停留在感觉层面。
返回列表