尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Scrapy+情感分析:豆瓣TOP250短评数据清洗与建模实战

Scrapy+情感分析:豆瓣TOP250短评数据清洗与建模实战 简介这是一份围绕豆瓣电影TOP250短评的完整数据挖掘与自然语言处理学习资源适合想掌握Scrapy爬虫、Pandas数据清洗、情感分析和中文文本建模的开发者。项目完整覆盖短评抓取、数据清理、特征工程与统计分析针对电影条目、短评和用户三套数据集展开多维排行、词云展示、地域分布及协同关系挖掘。情感建模部分给出朴素贝叶斯、支持向量机、快速文本、卷积网络、循环网络、门控循环单元等多种分类方案并以短评预测电影类型收尾。每个环节都配有可直接运行的爬虫脚本、中间数据、可视化图片和交互式笔记便于对照复现和理解特征提取、模型评估等关键步骤。资源共二百一十四个文件核心包括Python脚本、JSON中间数据、可视化PNG图片、Jupyter笔记等另有地理信息文件、训练模型与词向量文件压缩包约七十三兆目录结构清晰。目前已有三千七百七十四人学习下载适合具备Python基础、希望系统实战爬虫与自然语言处理的中高级学习者。1. 先别急着写爬虫TOP250 短评这个题目到底值不值得做这个标题把三件事串成了一条完整链路用 Scrapy 拿数据把拿回来的脏文本收拾干净再基于它训练一个中文文本情感分析模型。很多人一上来就盯着 Scrapy 写爬虫结果三天后卡在数据清洗上又一周后卡在模型效果上。TOP250 这个语料库的好处在于规模可控——250 部电影、每部几十到几百条短评总量在几万条量级单机就能跑完整个流程而且中文短文本里充满了演技在线剧情拉胯片尾反转救全片这种典型表达情感极性明显非常适合做情感分析的入门到进阶。适合谁想完整走一遍数据工程和文本建模的从业者或者打算把爬虫经验沉淀成数据产品的人。2. Scrapy 抓取 TOP250 短评从 URL 规律到请求调度的完整链路2.1 先探页面TOP250 的 URL 规律和短评入口写爬虫之前先花十分钟在浏览器里把页面结构摸清楚比什么都重要。豆瓣 TOP250 的列表页 URL 是https://movie.douban.com/top250?start0filterstart参数每 25 步进一次翻页规律是start0, 25, 50...一共 10 页。每部电影的详情页 URL 是列表页里a标签的href形如https://movie.douban.com/subject/1292052/中间那串数字就是电影的 subject id。短评页的入口在详情页的全部短评链接形如https://movie.douban.com/subject/1292052/comments?statusP。短评页本身的翻页参数要注意它不是按 25 走而是按start0, 20, 40走每页固定 20 条。这里有个坑未登录状态下短评页只能看到按热度排序的前 200 条左右再往后翻就没了这不是被反爬拦截而是产品设计上只对未登录用户开放这么多。所以数据量的第一上限就在这——250 部电影每部最多拿 200 条短评总量约 5 万条对情感分析建模其实已经够。如果非要多拿就得走登录态 Cookie那属于另一个范畴的工程。在写 Scrapy 之前我习惯先用 requests 把单页 HTML 拉下来看一眼确认短评在div.comment-item下面评论文本在span.short里用户名在span.comment-info的a里时间在span.comment-time的title属性里。这一步只要五分钟能省掉后面调 XPath 的大量试错。很多人跳过了这一步直接写 XPath结果因为少看了一个层级调试到半夜。2.2 最小可运行的 Scrapy 爬虫Item、Spider 与 Settings先定 Item。短评这活儿只需要五个字段加上一部电影的平均分做上下文信息# items.py import scrapy class DoubanCommentItem(scrapy.Item): movie_id scrapy.Field() # 电影 subject id movie_title scrapy.Field() # 电影名 movie_rating scrapy.Field() # 电影平均分后续做一致性校验用 user scrapy.Field() # 用户名 comment scrapy.Field() # 短评文本 comment_time scrapy.Field() # 发布时间字段没必要贪多votes有用数和回复数以后需要再加Scrapy 的 Item 是动态字段补一个scrapy.Field()就行。Spider 端我建议拆成两级页面来写先抓列表页拿到每部电影的详情页链接后再顺着详情页拿短评入口。这样做的好处是单条请求链路清晰出问题知道是列表层还是评论层挂的。# spiders/top250.py import re import scrapy from urllib.parse import urljoin from douban.items import DoubanCommentItem class Top250Spider(scrapy.Spider): name top250 allowed_domains [movie.douban.com] def start_requests(self): for start in range(0, 250, 25): url fhttps://movie.douban.com/top250?start{start}filter yield scrapy.Request(url, callbackself.parse_list) def parse_list(self, response): for movie in response.css(div.item): detail_url movie.css(a::attr(href)).get() title movie.css(span.title::text).get() yield scrapy.Request( detail_url, callbackself.parse_movie, meta{movie_id: detail_url.rstrip(/).split(/)[-1], movie_title: title}, ) def parse_movie(self, response): # 详情页抓平均分再找“全部短评”入口 rating_num response.css(strong.rating_num::text).get() comments_url response.css( a[href*comments]::attr(href)).get() if comments_url: yield scrapy.Request( urljoin(response.url, comments_url), callbackself.parse_comments, meta{**response.meta, movie_rating: rating_num}, ) def parse_comments(self, response): # 当前页没有评论块就停不再翻页 if not response.css(div.comment-item): return for sel in response.css(div.comment-item): item DoubanCommentItem() item[movie_id] response.meta[movie_id] item[movie_title] response.meta[movie_title] item[movie_rating] response.meta.get(movie_rating) item[user] sel.css(span.comment-info a::text).get() item[comment] sel.css(span.short::text).get() item[comment_time] sel.css( span.comment-time::attr(title)).get() # 空评论直接丢不给清洗环节添堵 if item[comment]: yield item # 翻页start 每次 20URL 里可能没有 start 参数 m re.search(rstart(\d), response.url) current_start int(m.group(1)) if m else 0 if m: next_url response.url.replace( fstart{current_start}, fstart{current_start 20}) else: sep if ? in response.url else ? next_url response.url f{sep}start20 yield scrapy.Request(next_url, callbackself.parse_comments, metaresponse.meta)逻辑说明response.meta是 Scrapy 在请求之间传数据的关键通道它把电影 id、片名从列表页一路带到短评页构造 Item 时不用回查关联表。{**response.meta, movie_rating: rating_num}是在原 meta 基础上追加字段Scrapy 的 Request 会复制一份 meta不会污染上一个请求。翻页代码里先判断当前页有没有评论块没有就直接 return避免对一个不存在的下一页发起无效请求这是很多入门爬虫翻车的地方——不设停止条件日志里全是 404 重试。Settings 才是让爬虫活过第一个小时的关键# settings.py BOT_NAME douban SPIDER_MODULES [douban.spiders] ROBOTSTXT_OBEY False # 教学级低频抓取生产环境先做合规评估 USER_AGENT ( Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0 Safari/537.36 ) DOWNLOAD_DELAY 2.0 # 每个请求间隔 2 秒 CONCURRENT_REQUESTS 8 CONCURRENT_REQUESTS_PER_DOMAIN 4 DOWNLOAD_TIMEOUT 15 RETRY_TIMES 2 FEED_EXPORT_ENCODING utf-8参数说明DOWNLOAD_DELAY是最重要的一个豆瓣对低频请求相对宽容实测 2 秒是安全下限CONCURRENT_REQUESTS_PER_DOMAIN 4意味着同一时间只有 4 个并发在跑配合延迟实际请求速率大约 2 条/秒整个 TOP250 全量跑完大概 40 到 60 分钟。FEED_EXPORT_ENCODING utf-8是新手最容易漏的不设这个scrapy crawl top250 -o comments.csv导出的 CSV 在 Excel 里打开就是一列乱码。提示ROBOTSTXT_OBEY False只是教学场景下的低频抓取配置真实部署前先确认目标站的 robots 协议和当地法规。低频、小规模、不商用是我做这类练习的三条底线。2.3 翻页与请求调度为什么先别上动态渲染很多人在接触爬虫时会习惯性问要不要用 Playwright 或 Selenium 处理动态 iframe我的回答是先把静态请求跑通再说。短评页的评论内容是服务端渲染的requests 直接拉就能拿到完整 HTMLresponse.css就能解析不存在 iframe 里的内容。只有登录态展开全部短评、或者某些页面需要点击展开更多才会触发 Ajax 加载那时候才轮到动态渲染登场。这里有个我走过的弯路第一次写这个爬虫时我直接上了 Playwright 无头浏览器每条短评都等渲染完成250 部电影跑了一整夜还封了几个请求。后来换回纯 Scrapy 静态请求配合 2 秒延迟一个小时出头就跑完数据量没有任何差别。所以判断标准很简单——先用 requests 或者浏览器的查看源代码看目标数据在不在原始 HTML 里在就别上动态渲染不在再考虑 Playwright 处理 JS 加载并把它只用在那几个需要交互的页面。请求调度上还有一个细节start_requests里直接构造了 10 个列表页 URL让 Scrapy 调度器自己排队。很多人在这里写成一个循环串行请求那样太慢。Scrapy 的调度器对同域请求会自动去重、自动排序配合CONCURRENT_REQUESTS_PER_DOMAIN就够了。你要做的只是把 URL 交给它别自作主张加线程。3. 短评数据清理与分析从能跑到能建模要过的三关3.1 Item Pipeline 里的去重与补全别让重复评论污染标签爬虫跑完第一轮得到的 CSV 里往往有重复项——同一个用户对同一部电影在翻页时可能被抓到两次或者详情页入口被重复访问。情感分析对重复数据很敏感如果某条吹爆的短评被抓了 20 次模型会以为这种表达在语料里占了 20% 的高频。所以去重是第一优先级。在 Item Pipeline 里做去重比在 DataFrame 里做更合适因为数据在源头就不该进库# pipelines.py import hashlib from scrapy.exceptions import DropItem class DeduplicatePipeline: def __init__(self): self.seen set() def process_item(self, item, spider): key hashlib.md5( f{item[user]}|{item[movie_id]}|{item[comment]}.encode(utf-8) ).hexdigest() if key in self.seen: raise DropItem(fduplicate: {key}) self.seen.add(key) return item逻辑说明去重键选了用户 电影 短评内容三个字段的拼接摘要而不是直接连 URL。因为同一个用户在同一部电影下理论上只会留一条短评时间字段可能有格式差异反而不适合进 key。用hashlib.md5拼成固定长度字符串内存里维护一个set就够了5 万条数据量级完全无压力。DropItem是 Scrapy 内置异常管线里抛出来就不会继续往下传。补全指的是把空字段处理掉。短评内容为空的比例不高但确实存在——有些用户只点看过不打字短评区会出现空文本。这类空评论我的处理方式是在 Spider 里直接过滤掉不要留给模型。另外comment_time也可能抓不到部分页面用相对时间显示刚刚5 分钟前title属性里才是完整时间抓取时优先取title取不到就留空后续统一处理。3.2 中文文本清理全角半角、HTML 实体与 emoji 的归一化这一节没有捷径每个规则都来自真实数据。短评文本最常见的三类脏数据全角字符混入尤其是标点、HTML 实体如amp;、以及 emoji 和特殊符号。下面这段是我常用的清洗函数# clean_text.py import re import unicodedata def normalize_text(text: str) - str: if not text: return # 1. 全角转半角保留中文和常用标点 text unicodedata.normalize(NFKC, text) # 2. 常见 HTML 实体 text text.replace(amp;, ).replace(quot;, ) # 3. 去掉 emoji 和零宽字符 text re.sub(r[\U0001F000-\U0001FAFF\uFE0F\u200B-\u200D\u2060], , text) # 4. 连续空白折叠成单空格 text re.sub(r\s, , text).strip() return text参数说明unicodedata.normalize(NFKC, text)会把全角字母数字折叠成半角同时对中文基本没有副作用是全角符号清理的首选方案。正则里的\U0001F000-\U0001FAFF覆盖 emoji 的主要区段\uFE0F是 emoji 的变体选择符\u200B是零宽空格这些都是短评里会实际出现的字符。第 4 步把所有换行和连续空格合成一个空格对情绪表达没有影响但能显著减少后续分词产出的低频碎片词。这里有个血泪经验不要把标点符号全部清掉。和的出现频次在情感分析里是有信号的连用多个感叹号往往意味着强烈情绪。我一般保留下半角?!和中文句读去掉其他符号。清洗完的文本放到 Excel 里随机抽 200 条人工读一遍比调十次正则都有用。3.3 短评的标签从哪来弱监督和人工标注的结合短评列表页有个很多人忽略的事实单条短评并不显示用户给这部电影打了几星只有有用和回复数。所以情感分析的标签不能直接从爬虫结果里拿到。我试过从看过页面去 join 星级页面结构不稳定而且未登录状态访问受限性价比很低。实际工程里最稳的路线是混合方案先让 SnowNLP 给全部短评算一个 0 到 1 的情感分把分数极度靠近两端的样本当成伪标签中间地带人工标注 800 到 1000 条作为最终训练集的主体。import pandas as pd from snownlp import SnowNLP df pd.read_csv(comments_cleaned.csv) df[snownlp_score] df[cleaned_comment].apply( lambda t: SnowNLP(t).sentiments ) # 小于 0.2 视为负面大于 0.8 视为正面 negative_mask df[snownlp_score] 0.2 positive_mask df[snownlp_score] 0.8 df[pseudo_label] None df.loc[negative_mask, pseudo_label] 0 df.loc[positive_mask, pseudo_label] 1 # 抽取不确定的样本交给人工标注 uncertain df[(df[snownlp_score] 0.2) (df[snownlp_score] 0.8)] uncertain.sample(800, random_state42).to_csv(manual_label_need.csv, indexFalse)逻辑说明伪标签这一段的价值不是直接得到高质量训练集而是把语料切成三块——确定负、确定正、不确定。确定的两块可以拿去预训练一个弱分类器不确定的 800 条人工标注后混入训练这样人工标注成本被压到最低而分类器的边界又由人工标注定义不会被 SnowNLP 的偏差带跑。人工标注完成后把标签合并回主表manual_labels pd.read_csv(manual_label_done.csv) df df.merge( manual_labels[[user, movie_id, comment, manual_label]], on[user, movie_id, comment], howleft, )这里用user movie_id comment做关联键和去重逻辑保持一致避免 join 错位。注意标注文件里要保留原始文本因为清洗后的文本可能已经丢了一些信息但关联键必须用原始评论内容才能对上号。3.4 标签就绪后先做一轮 EDA分布决定了模型的上限标签拿到手不要急着训练。先看三个分布标签占比、短评长度分布、每部电影的评论量。我遇到过一个典型情况——短评平均长度 40 字中位数 30 字但模型在 TF-IDF 阶段产出的特征里大量高频词是电影一部看了全是噪声。这不是模型问题是没做停用词过滤。import matplotlib.pyplot as plt print(df[manual_label].value_counts(normalizeTrue)) df[len] df[cleaned_comment].str.len() print(df.groupby(manual_label)[len].describe()) df[len].hist(bins30) plt.savefig(comment_len_dist.png)如果value_counts显示正面占比超过 60%后面所有评估都要警惕一个全部预测正面的模型也能拿到 0.6 的准确率看起来很美实际毫无价值。如果长度分布显示大量短评集中在 10 字以内说明语料大多是很极端的好看烂片模型会偏向学习强极性词对 50 字以上的转折句天然薄弱。这时候就该考虑是不是要把长短评分开建模或者至少保证训练集里长文本占比不被压缩。另外 TOP250 这个榜单本身有偏——能进前 250 的电影整体口碑偏正面抓下来的短评里好评天然占多数。这个偏斜会直接传导给模型所以后面评估时必须按类别看指标别被整体的高准确率骗过去。4. 构建中文文本情感分析模型从 SnowNLP 基线到微调 BERT 的选型路线4.1 先用 SnowNLP 跑一条基线知道下限在哪才知道模型有没有进步3.3 里已经用 SnowNLP 给全量短评打了分现在把它当成候选模型来体检。做法很简单拿人工标注的数据当测试集算 SnowNLP 硬标签和人工标签的一致率。这个数字就是整个项目的下限参照。from sklearn.metrics import accuracy_score, classification_report manual df.dropna(subset[manual_label]) y_snow (manual[snownlp_score] 0.5).astype(int).tolist() print(accuracy_score(manual[manual_label], y_snow)) print(classification_report( manual[manual_label], y_snow, target_names[neg, neu, pos], ))说明SnowNLP 默认模型是在电商语料上训练的对演技在线叙事节奏好这类电影领域词汇缺乏训练而电商语料里高频的快递客服又落在无关维度。所以在电影短评上这个基线准确率通常只有六成上下其中负面类的召回率尤其低。得到这个数之后你知道了两件事一是这个任务不是一眼能看穿的线性问题二是后面任何模型至少要超过这个数字才有存在价值。顺带一提纯文本情感分析和多模态情感分析的差别就在这——多模态可以靠图像和音频辅助判断反讽和语气纯文本基线就只能先把字面意思吃透。4.2 TF-IDF LinearSVC数据量在几千条时最稳的组合当人工标注样本在 1500 到 5000 条区间时TF-IDF 加线性 SVM 是我最常用的组合原因有三特征可解释、训练快、调参门槛低。对中文短文本字级别加词级别混合的 TF-IDF 往往比纯词级别好因为短评里新词和错别字多拆成字可以兜底。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.svm import LinearSVC from sklearn.pipeline import make_pipeline from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report X df[cleaned_comment].tolist() y df[manual_label].astype(int).tolist() # 只取已标注样本 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) pipe make_pipeline( TfidfVectorizer( analyzerchar_wb, ngram_range(1, 2), min_df5, max_features50000, sublinear_tfTrue, ), LinearSVC(C1.0, class_weightbalanced, max_iter2000), ) pipe.fit(X_train, y_train) y_pred pipe.predict(X_test) print(classification_report(y_test, y_pred, target_names[neg, neu, pos]))参数说明analyzerchar_wb表示按字符切分并保留词语内部的相邻字符ngram_range(1, 2)取单字和双字组合相当于把拉胯这类双字词也纳入特征min_df5过滤掉只在少于 5 条里出现的字组能压掉大量错别字噪声sublinear_tfTrue对词频取对数缩放避免电影这种超高频词主导距离。C1.0是线性 SVM 的正则强度数据量小就调低一点比如 0.5 或 0.1防止过拟合class_weightbalanced应对好评占比高的问题让少数类在损失函数里拿到更高权重。代码跑完先看classification_report里的 macro F1 而不是准确率。三分类任务里如果负面类的 F1 只有 0.5说明模型在关键业务类别上没学会需要回去补数据或者调特征而不是调正则。另外注意train_test_split里的stratifyy不设这个参数切分后训练集和测试集的类别比例可能完全失衡小样本场景下后果很严重。4.3 微调 bert-base-chinese什么时候值得上预训练模型当人工标注数据超过 5000 条或者业务要求对反讽、转折句等复杂表达做出区分时该考虑预训练模型了。中文文本情感分析最常见的底座是bert-base-chinese用 Hugging Face 的transformers库微调训练脚本本身不长但坑都在数据准备和训练策略上。from transformers import AutoTokenizer, AutoModelForSequenceClassification model_name bert-base-chinese tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForSequenceClassification.from_pretrained( model_name, num_labels3 ) def tokenize_fn(batch): return tokenizer( batch[text], truncationTrue, max_length128, paddingmax_length, )参数说明max_length128对短评足够短评 UI 上限是 140 字留出边界符号的量truncationTrue保证超长文本被截断而不是报错。训练侧几个关键参数learning_rate2e-5、batch_size16、epochs3起步warmup_ratio0.1。数据量少时要开weight_decay防过拟合一般取 0.01 到 0.05。微调预训练模型在几千条数据上的提升通常有几个点对奔着 XX 去的结果……这类句式理解更准对领域词视听语言蒙太奇能借助预训练知识兜底在否定句上的表现比 TF-IDF 明显好。但代价是训练和推理都慢了一个量级而且显存不够时 batch size 必须调小学习率也要跟着往下降否则 loss 会震荡。如果跑 BERT 只是为了把准确率从 0.72 提到 0.75但推理时间从毫秒级变成秒级就得自己掂量业务值不值。4.4 模型评估按类别看 F1别被整体准确率骗了评估是新手最容易自欺欺人的环节。因为 TOP250 短评里好评天然多一个把所有样本都预测成正面的模型准确率也能到 0.55 以上。所以评估必须看三件事classification_report里每一类的 F1、混淆矩阵的行列分布、以及按电影维度聚合后的人工抽检。混淆矩阵里最值得警惕的是中性这一行——短评里还行一般凑合这类表达本来就暧昧模型大概率把它分到正面去因为正面样本多、先验强。如果中性类 F1 低于 0.3与其继续调模型不如先把标注规范改成二分类把中性合并进其他类或者单独建一个情感强度回归任务。别把三分类当成不可动摇的设定任务是数据定义的。另一个实践技巧是把错误样本导出成 CSV按电影分组看。你会看到很大一片错误集中在某几部争议电影上——比如一部两极分化严重的电影正面和负面短评措辞都极端模型却全归成了正面。这种错误不是模型学不会而是该电影的短评本身带有领域特殊性。把这些样本加进训练集比调任何超参数都有效。5. 避坑与排错爬虫断流、评论乱码、模型误判的四类现场5.1 爬虫断流下载超时和 403 是两码事现象跑着跑着日志里全是Retrying GET ...或者连续出现 403 响应爬虫最终停在一个页面上不走了。原因分两类。第一类是网络层面DOWNLOAD_TIMEOUT设得太短网络链路波动时请求在设定时间内没回来就被判失败触发重试重试还失败就直接丢请求。第二类是服务端风控同一 IP 请求频率超过阈值返回 403。从日志能看出区别——超时是TimeoutError403 是IgnoredResponse。解决先把DOWNLOAD_TIMEOUT从默认的 180 秒调低到 15 秒让超时快速失败而不是每个请求都卡三分钟再把DOWNLOAD_DELAY从 2 秒提到 4 秒观察 403 是否消失。很多人一遇到 403 就去换 User-Agent 池实际上对豆瓣这种级别的反爬频率降下来比 UA 轮换管用得多。我用 2 秒延迟连续跑过几千条请求没有触发风控一旦并发拉满或者把延迟降到 0.5 秒30 分钟内必出 403。5.2 评论乱码与截断编码和字段长度的双重陷阱现象导出的 CSV 在 Excel 里打开全是乱码或者短评文本只有一半。先说乱码这有两个不同环节一是 Scrapy 输出 CSV 时默认编码不是 UTF-8前面 settings 里FEED_EXPORT_ENCODING utf-8就是干这个的二是网页本身返回的编码与 Scrapy 自动检测不一致有些老页面会带gb2312声明Scrapy 的Response.text会按 header 里的 charset 解码一旦解错就会出现â€这类乱码。这种问题在 Spider 里用response.encoding utf-8强设编码能解决。截断问题更隐蔽豆瓣短评的 UI 上限是 140 字但 HTML 里抓到的文本有时候只有 70 字原因是页面默认折叠超长短评需要点击展开才能看到全文。这种折叠内容在原始 HTML 里可能直接缺失也可能带上隐藏节点。我的处理办法是抓完span.short的文本后如果发现以…结尾就把该条标记为截断建模时单独做特征或者直接丢弃避免模型学到以省略号结尾的都是好评这种假规律。注意清洗阶段每做一次转换就在数据里加一个raw_comment字段保留原始文本。模型效果不好时回看原始文本比盯着清洗后的文本排错快得多。这算是我的一个习惯也是踩坑换来的后悔药。5.3 情感模型翻车否定句、反讽和领域词汇现象模型把不是不好看是太短了判成负面把太好看了哭得我眼睛疼也判成负面。这两类错误本质不一样。前者是逻辑否定TF-IDF 把不是不好看当不好看处理是正常的因为特征里好看权重太高覆盖了不是的语义解决方向是让模型看到更长上下文ngram_range(1, 2)可以调到(1, 3)或者直接换 BERT。后者是反讽属于情感分析里公认的难题主流做法是在标注阶段就给反讽样本单独标一类让模型至少有学习目标而不是指望预训练模型自动理解。领域词汇问题用一个例子说明短评里高频出现的拉胯封神翻车都是近几年的网络语言通用情感词典里根本没有。这类词没有捷径只能靠数据里的频次让模型自己学。如果频次太低少于 5 次min_df5会把它们当噪声过滤掉所以遇到这类词泛滥的语料把min_df降到 3 再试一轮是合理的。5.4 数据偏斜为什么还行永远是最难分的那一类现象模型对正面和负面的 F1 都在 0.85 以上但中性的 F1 只有 0.2甚至很多中性短评被分到负面。原因有两层。第一层是采样偏斜TOP250 的短评里好评比例天然高人工标注抽的 800 条不确定样本里中性占比不低但整个训练集里中性可能只占 15%。第二层是标注分歧标注者自己都说不清还行算中性还是轻微正面这部分标注噪声直接限制了模型上限。解决先用class_weightbalanced把少数类权重提上来看中性 F1 是否改善如果没改善检查标注规范是不是太模糊把还行一般凑合全归到中性把不错可以归到正面缩小区间。实在分不动就回到二分类加一个置信度阈值——低于阈值的样本不输出情感标签而是打上待确认。很多业务场景里告诉用户这条短评情绪不明确比给一个错的极性更有价值。6. 收个尾把模型输出还原成业务结论的两个验证技巧6.1 评分一致性校验用电影均分反推模型有没有学到真东西模型训完、F1 达标不等于事情做完了。我习惯做两个验证一是交叉验证模型输出和客观数据的一致二是把模型的判断依据拉出来人工过一眼。这两个技巧能帮你判断模型是学对了还是蒙对了。第一个技巧叫评分一致性校验每部电影有平均分模型对它的所有短评打分后取均值两边应该呈正相关。在 pandas 里按电影聚合把平均分和短评平均情感分做 Spearman 相关df[movie_rating] pd.to_numeric(df[movie_rating], errorscoerce) movie_stats ( df.groupby(movie_title) .agg(avg_rating(movie_rating, first), avg_senti(pred_score, mean)) ) corr movie_stats[avg_rating].corr(movie_stats[avg_senti], methodspearman) print(corr)系数在 0.4 以上说明模型的整体判断靠谱低于 0.2就要警惕模型学到的不是情感而是某种和电影类型混杂的噪声。这个验证不消耗额外的标注成本是模型上线前性价比最高的一关。6.2 错误样本回看把黑匣子打开看一眼第二个技巧是看错误样本。每次训完模型把预测错误的那几十条短评打出来逐条问自己如果是人我能分对吗如果能模型为什么不能通常答案是特征没覆盖到如果看着也犹豫那这条样本就该从评估集里剔除它属于标注噪声。这一步会花掉半小时但收获比再调两轮参数大得多。我现在养成的习惯是所有模型上线前必须附一个 20 条的错误样本清单写在项目注释里作为这个版本的黑匣子说明书。每次迭代先看这个清单再跑训练效率比盲目调参高得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表