ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Python的作者归属实战:从功能词到字符N元语法

基于Python的作者归属实战:从功能词到字符N元语法 简介本资源为《Python数据挖掘项目开发实战》第9章「作者归属问题」的配套PDF面向具备一定Python与数据挖掘基础、希望完整走通分类项目流程的开发者与学习者。内容围绕文本挖掘中的作者归属任务展开从问题界定、背景知识到特征抽取、流水线搭建与结果分析系统讲解如何把交叉验证、特征工程与分类算法整合应用。资源重点比较功能词与N元语法两类特征的效果并涉及词袋模型回顾、支持向量机、数据集清洗等关键环节实验数据先采用古腾堡计划图书语料再进阶到噪音较多的真实电子邮件语料同时区分封闭问题与开放问题兼顾作者画像、作者验证、作者聚类等衍生场景。包内共1个PDF文件大小约1.48MB便于随时查阅与对照实践。目前已有369人学习适合希望掌握数据挖掘完整流程、提升文本分类实战能力的读者参考。1. 作者归属问题从177本公版书到安然邮件语料这套分类流水线到底怎么跑手里有一堆匿名文档想知道它们分别出自谁手——这事在文本挖掘里叫作者归属authorship attribution本质是一个多分类任务。它不靠写作时间、印刷形式或笔迹只用作品内容本身从一组候选作者里找出文档真正的主人。听起来像玄学但背后的依据是文体学每个人在语言掌握上有微小差异这些差异会稳定地反映到用词、标点和句式习惯里机器恰好擅长统计这些细微差别。这份《Python数据挖掘项目开发实战》第9章的配套资源把前几章的数据挖掘方法整合成一条完整流水线界定问题、抽取特征、创建Pipeline、交叉检验、对比特征效果。它用两套数据集递进——先用古腾堡计划的177本公版图书7位作家跑通流程再上噪音更多的安然公司真实邮件语料增加难度。适合已经会sklearn基础操作、想找一个完整分类项目练手的人也适合想搞清楚功能词和字符N元语法到底哪个更适合作者归属的从业者。下面按“资源是什么→怎么用→坑在哪”的顺序拆开讲。2. 图书数据集从古腾堡下载到清洗把177本作品变成可训练矩阵2.1 下载与目录结构数据来自古腾堡计划www.gutenberg.org7位作家共177篇作品塔金顿22篇、狄更斯44篇、内斯比特10篇、柯南·道尔51篇、马克·吐温29篇、伯顿11篇、加博里奥10篇。配套代码包里有一个getdata.py负责自动下载并按作者分文件夹存放。先在Data目录下建好books文件夹路径要和代码里保持一致import os import sys data_folder os.path.join(os.path.expanduser(~), Data, books)然后在笔记本里加载下载脚本并运行# 在Jupyter单元格中执行加载并运行getdata.py !load getdata.py!load是IPython的魔术命令把脚本内容读进当前单元格再按ShiftEnter执行下载逻辑。下载耗时取决于网络跑完会给出提示。下载完成后books目录下是按作者名命名的子文件夹每个子文件夹里是该作家的作品文本文件。提示下载脚本依赖requests库如果环境里没有先pip install requests。下载过程中断的话重新运行会覆盖已存在的文件不会重复堆积。2.2 清洗为什么不在磁盘上直接删声明每篇古腾堡作品前后都有一大段项目声明文字从数据分析角度看全是噪音。直觉做法是打开文件把声明删掉再保存但这样做有个隐患一旦改动原文件后续实验就无法保证可重现——你没法证明这次结果和上次用的是同一份数据。更稳妥的做法是在加载时跳过声明部分原文件保持不动。清洗函数按行切分找到作品开始和结束的标识行取中间内容def clean_book(document): lines document.split(\n) start 0 end len(lines) for i in range(len(lines)): line lines[i] if line.startswith(*** START OF THIS PROJECT GUTENBERG): start i 1 elif line.startswith(*** END OF THIS PROJECT GUTENBERG): end i - 1 return \n.join(lines[start:end])start初始为0、end初始为总行数是为了兜底——万一某篇作品没有标准标识行就返回全文而不是空字符串。找到标识行后start取标识行的下一行end取标识行的上一行中间就是纯作品内容。最后用换行符重新拼接。2.3 加载全部图书并生成类别标签加载函数遍历books下的作者子文件夹用enumerate给每个作者分配一个整数编号这个编号就是分类任务里的类别标签import numpy as np def load_books_data(folderdata_folder): documents [] authors [] subfolders [subfolder for subfolder in os.listdir(folder) if os.path.isdir(os.path.join(folder, subfolder))] for author_number, subfolder in enumerate(subfolders): full_subfolder_path os.path.join(folder, subfolder) for document_name in os.listdir(full_subfolder_path): with open(os.path.join(full_subfolder_path, document_name)) as inf: documents.append(clean_book(inf.read())) authors.append(author_number) return documents, np.array(authors, dtypeint)documents是文本列表authors是对应的类别数组。enumerate保证每个子文件夹对应唯一编号np.array(..., dtypeint)把类别转成整型数组方便后续传给sklearn的分类器。调用一次就能拿到全部数据documents, classes load_books_data(data_folder)177篇文本加载进内存没有压力。如果数据集大到内存装不下常见做法是每次只从一篇或几篇文档抽特征把特征矩阵写到磁盘或内存映射文件里而不是一次性把原始文本全读进来。3. 功能词特征用CountVectorizer锁定写作习惯跑通第一条流水线3.1 功能词为什么能区分作者功能词是本身含义很弱、但构成英语句子必不可少的成分比如this、which、the、of。它们的用法更多由作者的使用习惯决定而不是由文档主题决定。一个典型例子很多美国人很在意区分that和which的用法澳大利亚等国家的人就不太在意有些人几乎一律用that。这类微小差异叠加起来就成了区分作者的信号。选功能词有个经验法则优先选在所有文档中使用频率都高的词。使用越频繁对作者分析提供的信息越多使用频率低的词更适合做基于内容的主题划分而不是作者归属。下面这份功能词表来自已发表研究可以直接用function_words [a, able, aboard, about, above, absent, according, accordingly, across, after, against, ahead, albeit, all, along, alongside, although, am, amid, amidst, among, amongst, amount, an, and, another, anti, any, anybody, anyone, anything, are, around, as, aside, astraddle, astride, at, away, bar, barring, be, because, been, before, behind, being, below, beneath, beside, besides, better, between, beyond, bit, both, but, by, can, certain, circa, close, concerning, consequently, considering, could, couple, dare, deal, despite, down, due, during, each, eight, eighth, either, enough, every, everybody, everyone, everything, except, excepting, excluding, failing, few, fewer, fifth, first, five, following, for, four, fourth, from, front, given, good, great, had, half, have, he, heaps, hence, her, hers, herself, him, himself, his, however, i, if, in, including, inside, instead, into, is, it, its, itself, keeping, lack, less, like, little, loads, lots, majority, many, masses, may, me, might, mine, minority, minus, more, most, much, must, my, myself, near, need, neither, nevertheless, next, nine, ninth, no, nobody, none, nor, nothing, notwithstanding, number, numbers, of, off, on, once, one, onto, opposite, or, other, ought, our, ours, ourselves, out, outside, over, part, past, pending, per, pertaining, place, plenty, plethora, plus, quantities, quantity, quarter, regarding, remainder, respecting, rest, round, save, saving, second, seven, seventh, several, shall, she, should, similar, since, six, sixth, so, some, somebody, someone, something, spite, such, ten, tenth, than, thanks, that, the, their, theirs, them, themselves, then, thence, therefore, these, they, third, this, those, though, three, through, throughout, thru, thus, till, time, to, tons, top, toward, towards, two, under, underneath, unless, unlike, until, unto, up, upon, us, used, various, versus, via, view, wanting, was, we, were, what, whatever, when, whenever, where, whereas, wherever, whether, which, whichever, while, whilst, who, whoever, whole, whom, whomever, whose, will, with, within, without, would, yet, you, your, yours, yourself, yourselves]把这份列表传给CountVectorizer的vocabulary参数就锁定了特征空间——只统计这些词不再从数据里学习词汇表from sklearn.feature_extraction.text import CountVectorizer extractor CountVectorizer(vocabularyfunction_words)注意传了vocabulary之后训练集里没出现的功能词会被忽略测试集里出现但词汇表里没有的词也不会被统计。这保证了训练和预测阶段用的是同一套特征不会因为数据不同导致维度错位。3.2 支持向量机与网格搜索分类器用SVM。它的基本思想是找一个能把两类数据分开的最优超平面让每个点到超平面的距离最大化。多分类时常见做法是为每个类别建一个一对多分类器把“属于该类”和“其他所有类”分开预测时选最匹配的那个。scikit-learn的SVC会自动处理这个过程。SVM有两个关键参数kernel和C。kernel决定用什么核函数线性核最简单rbf核高斯核适合线性不可分的数据。C控制对误分类的惩罚C值越高间隔越小尽量把所有训练数据分对但有过拟合风险C值越低间隔越大允许一些数据分错泛化可能更好。用网格搜索在这两个参数上找最优组合from sklearn.svm import SVC from sklearn.cross_validation import cross_val_score from sklearn.pipeline import Pipeline from sklearn import grid_search parameters {kernel: (linear, rbf), C: [1, 10]} svr SVC() grid grid_search.GridSearchCV(svr, parameters)GridSearchCV会遍历kernel和C的所有组合对每组做交叉检验选平均得分最高的那组。parameters字典里kernel是元组、C是列表这是网格搜索的标准写法。提示rbf核只适合数据集相对较小的情况比如特征数少于10000。功能词表大约200个词特征维度不高rbf完全扛得住。但如果换成字符N元语法特征数会暴涨得留意。3.3 组装流水线并交叉检验把特征抽取和分类器串成Pipeline避免手动分步调用时训练集和测试集特征不一致的问题pipeline1 Pipeline([ (feature_extraction, extractor), (clf, grid) ]) scores cross_val_score(pipeline1, documents, classes, scoringf1) print(Score: {:.3f}.format(np.mean(scores)))cross_val_score默认做3折交叉检验每折里Pipeline会先在训练折上fit特征抽取器再在测试折上transform保证不泄露测试集信息。用f1作为评分标准是因为7个作者的类别分布不完全均衡f1比准确率更能反映每个类别的分类质量。这套功能词流水线的正确率大约0.811对7个作者来说已经不错。4. 字符N元语法换掉特征抽取器看稀疏矩阵怎么提升分类效果4.1 字符N元语法的原理与参数N元语法是由N个连续对象组成的序列对文本来说N通常取2到6。基于单词的N元语法常用于主题相关研究而基于字符的N元语法在作者归属上效果很好。广为接受的理论是人们写作时倾向于选自己讲起来容易的单词字符N元模型N取2到6跟音素组成单词发音的声音有很好的相似关系因此能模拟写作风格。用CountVectorizer抽取字符N元语法关键是设置analyzerchar和ngram_rangepipeline Pipeline([ (feature_extraction, CountVectorizer(analyzerchar, ngram_range(3, 3))), (classifier, grid) ]) scores cross_val_score(pipeline, documents, classes, scoringf1) print(Score: {:.3f}.format(np.mean(scores)))analyzerchar告诉向量化器按字符切分而不是按词切分。ngram_range(3, 3)表示只抽取长度为3的字符N元语法两个值相同就锁定单一长度。如果想同时抽2到4的N元语法写成(2, 4)即可但特征维度会成倍增长。字符N元语法矩阵的一个显著特点是稀疏。N取2时大约75%的项为0N取5时93%以上为0。不过比起基于词语的N元语法矩阵稀疏程度还是低一些常见分类器处理起来问题不大。4.2 功能词与字符N元语法的差异两者存在大量隐式重合因为字符序列更可能出现在功能词中。但差别也很明显字符N元语法能捕获标点的使用特点功能词做不到。比如句号在字符N元语法里是一个特征而基于功能词的方法只能用句号前的单词。这意味着字符N元语法对作者的标点习惯敏感而功能词对语法词选择敏感。实际选型时如果文本经过严格清洗、标点规范两种特征都可以试如果文本标点混乱或作者有独特标点习惯字符N元语法往往更有优势。代价是特征维度高、训练慢需要更多内存。4.3 参数怎么调ngram_range的N值选择没有万能公式。N太小比如2特征区分度不够N太大比如6特征矩阵极度稀疏很多N元语法只出现一两次对分类帮助有限还增加计算量。常见做法是从3开始试再对比2、4、5的效果。CountVectorizer还有几个参数值得关注lowercase默认True会把所有字符转小写如果作者的大小写习惯是区分信号可以设为Falsemax_features可以限制特征数量按词频取前N个适合特征爆炸时降维min_df可以过滤掉出现次数太少的N元语法减少噪音。注意字符N元语法对文本长度敏感。短文本抽不出足够多的N元语法特征矩阵会非常稀疏分类效果可能不如功能词。安然邮件里有些邮件很短这时候要留意样本长度分布。5. 安然邮件语料从423MB原始邮件到均衡的作者分类数据集5.1 数据集背景与获取安然公司曾是全球最大的能源公司之一2000年拥有20000余名员工年收入高达1000亿美元。2001年财务造假丑闻曝光后股价从90多美元跌到1美元随即破产。调查过程中公开了60多万封电子邮件这些数据后来被广泛用于社会媒体分析、欺诈分析等研究。用于作者归属的优势在于能明确知道发件人是谁语料规模远大于图书数据集。数据集从卡内基梅隆大学网站下载整个包423MBgzip压缩格式。非Linux系统可以用7-zip解压。解压后目录默认为enron_mail_20110402。指定数据位置enron_data_folder os.path.join( os.path.expanduser(~), Data, enron_mail_20110402, maildir)5.2 邮件解析与类别均衡做作者归属只需要明确知道发件人的邮件所以查看每位用户的发件箱——文件夹名里含“sent”的那些。邮件内容需要用解析器从原始邮件里抽出来from email.parser import Parser p Parser()加载函数要解决一个现实问题有些用户发了几千封邮件有些只发了几十封类别严重不均衡。用三个参数控制min_docs_author指定每个发件人至少发过多少封才纳入max_docs_author指定最多从每个用户抽多少封num_authors限定作者数量。为了实验结果可重现还要固定随机状态from sklearn.utils import check_random_state def get_enron_corpus(num_authors10, data_folderdata_folder, min_docs_author10, max_docs_author100, random_stateNone): random_state check_random_state(random_state) email_addresses sorted(os.listdir(data_folder)) random_state.shuffle(email_addresses) documents [] classes [] author_num 0 authors {} for user in email_addresses: users_email_folder os.path.join(data_folder, user) mail_folders [os.path.join(users_email_folder, subfolder) for subfolder in os.listdir(users_email_folder) if sent in subfolder] # 后续遍历mail_folders解析邮件按min/max限制抽取 # 满足条件的作者才分配author_num并加入authors字典 return documents, np.array(classes, dtypeint), authors先sorted再shuffle是有意为之os.listdir每次返回顺序不一定相同先排序保证基础顺序一致再用固定随机状态打乱这样只要random_state相同每次选出的作者和邮件就完全一样。authors字典把用户名和类别编号关联起来数据挖掘过程用不到但可视化时能定位到具体是谁。5.3 在邮件语料上跑流水线拿到documents和classes后直接复用前面的Pipeline。功能词流水线和字符N元语法流水线都可以跑对比f1得分。邮件语料的噪音比图书大得多——有签名档、转发内容、回复引用、拼写错误这些都会影响特征质量。常见做法是先做一轮轻量清洗去掉邮件头、去掉引用行以“”开头的行、统一换行符。但清洗过度也可能抹掉作者的个人习惯比如有人就是爱用特定签名格式这本身就是信号。提示安然数据集里邮件正文的编码格式不统一有些是latin-1有些是utf-8。读取时如果报编码错误用errorsignore或errorsreplace兜底别让个别邮件中断整个加载过程。6. 避坑与排查作者归属流水线里最容易翻车的五个地方6.1 交叉检验得分高得离谱现象功能词流水线在图书数据集上f1超过0.95换到邮件语料掉到0.6以下。原因图书数据集里同一作者的多部作品被同时分到训练折和测试折模型可能记住了某部作品的特定内容而不是作者的通用风格。邮件语料里同一作者的邮件主题差异大模型泛化能力暴露出来。解决按作者分组做交叉检验确保同一作者的作品不会同时出现在训练和测试折。sklearn的GroupKFold可以指定分组标签把作者编号作为分组依据。6.2 CountVectorizer的vocabulary和训练集不匹配现象预测时报维度错误或者某些特征全为0。原因手动传了vocabulary但训练集里实际出现的词和词汇表对不上或者训练时用了一套词汇表预测时换了另一套。解决把CountVectorizer放进Pipeline里让它在每折训练时自己fit。不要在外面先fit好再传进去否则交叉检验时特征抽取器已经见过全部数据造成信息泄露。6.3 字符N元语法内存爆掉现象ngram_range设成(2, 6)后程序卡死或报MemoryError。原因字符N元语法的特征数量随N值指数增长177本书的字符总量很大同时抽2到6的所有N元语法会产生海量特征。解决先用单一N值比如3跑通再逐步增加。用min_df5过滤掉出现次数少于5的N元语法能大幅降低维度。或者用HashingVectorizer替代它不需要维护词汇表内存占用更可控。6.4 邮件解析后正文为空现象加载安然邮件后很多documents元素是空字符串或只有几个字符。原因邮件是多部分MIME结构正文可能在某个子部分里直接取msg.get_payload()拿到的是附件或HTML部分。有些邮件正文是base64编码没解码就是乱码。解决用email库的walk()方法遍历所有部分判断content_type是否为text/plain是的话取get_payload(decodeTrue)再解码。遇到text/html可以跳过或做标签清洗。6.5 随机状态没固定导致结果不可重现现象同样的代码跑两次f1得分不一样。原因get_enron_corpus里用了随机打乱选作者但random_state传了None或者SVM的random_state没设某些核函数内部有随机初始化。解决加载数据时固定random_state比如传42。SVC的random_state也设成固定值。train_test_split、cross_val_score如果有随机成分同样要固定。把随机种子写进配置别散落在代码各处。7. 进阶技巧用混淆矩阵定位“谁被认成了谁”再回头调特征跑完流水线拿到f1得分只是开始真正有价值的是看模型在哪些作者之间混淆。用混淆矩阵能直观看到哪些作者的文档被大量分错错分给了谁。这一步往往能反过来指导特征工程。from sklearn.metrics import confusion_matrix from sklearn.cross_validation import cross_val_predict # 用cross_val_predict拿到每折的预测结果避免手动分折 predicted cross_val_predict(pipeline1, documents, classes, cv3) cm confusion_matrix(classes, predicted) print(cm)cross_val_predict返回的是每个样本在它所在测试折上的预测类别拼接起来就是全量预测。confusion_matrix的行是真实类别列是预测类别对角线是分对的非对角线是分错的。如果发现作者A和作者B之间互相错分特别多说明这两个人的写作风格在所选特征下太接近。这时候有几个调整方向。一是换特征功能词分不开的试试字符N元语法特别是加入标点相关的N元语法。二是调SVM参数如果混淆集中在某几类可能是C值太大导致过拟合或者核函数不合适可以扩大网格搜索范围。三是检查数据有没有同一作者的文档被错误标注成另一个作者或者某个作者的文档里混入了别人的作品。我一般会先看混淆矩阵的对角线占比如果某类召回率明显低于其他类优先查那类的数据质量。有一次跑图书数据集柯南·道尔的作品被大量分给狄更斯查下来发现下载的某几本书其实是合集里面混了狄更斯的作品。数据问题不解决调参调到天亮也没用。从那以后我每次跑作者归属之前都强制走一遍混淆矩阵先确认数据层面没有明显错标再动特征和参数。希望帮到你。本文还有配套的精品资源点击获取
返回列表