简介:一份以《流浪地球》猫眼平台真实用户评论为实例的循环神经网络情感分析项目,面向自然语言处理、深度学习方向的初中级学习者及影视舆情分析人员,旨在解决影评数据量大、人工标注困难时如何利用RNN自动完成情感极性三分类(正面、中立、负面)识别与结果分析的问题。压缩包共25个文件、约8.99MB,文件类型涵盖4个Python源码、2个Jupyter Notebook、2个CSV数据文件,以及图片、说明文档等;源码目录中内置d2l深度学习组件,支持PyTorch、MXNet、TensorFlow等后端切换,便于在不同环境中运行。另有Pyc编译文件与多张可视化图片,可帮助快速查看模型训练过程中的特征与结果。目前资源已有72人学习下载,适合希望从数据采集到建模评估完整走通文本情感分析流程的读者。通过该资源可复现爬虫采集、评论清洗、分词、词向量嵌入及RNN训练评估的完整链路,附带的说明文件和数据也便于理解实验设定、核心代码逻辑以及三分类结果,对后续拓展到其他影视作品的评论情感挖掘有直接参考价值。
1. 为什么用《流浪地球》猫眼影评和 RNN 做三分类情感识别
一句话说清这个标题在干什么:从猫眼平台抓取《流浪地球》的用户短评,依次做数据清洗、预处理、中文分词、词向量嵌入,再交给 RNN 循环神经网络做训练评估,最终输出正面、负面、中性三分类情感识别结果。它本质上是一条完整的文本挖掘流水线,覆盖了网络爬虫、文本表示和深度学习三块内容。选《流浪地球》当语料有实际好处:短评量大,正负情感表达都很鲜明,中性评论也足够多,用来调三分类比那些冷门或一边倒的影片舒服得多。适合课程设计、毕业设计,也适合想在内容平台里快速验证评论挖掘价值的从业者。
2. 网络爬虫抓评论、清洗预处理与中文分词:语料先收拾干净,后面才能少返工
2.1 猫眼评论接口定位、请求参数与重试策略
常见做法是从浏览器开发者工具里抓 XHR 接口。打开《流浪地球》的猫眼电影详情页,按 F12 切到 Network 面板,过滤 comment 或 review 关键字,下拉评论区就能看到真正的评论接口。接口地址通常是一串带 movieId、offset、limit 参数的 HTTP 请求,返回 JSON 结构。不要拿页面 HTML 去解析,页面改版你的代码就废了,直接拿 JSON 字段最稳定。
请求参数里基本绕不开 movieId(这个电影的全局 ID)、offset(偏移量)、limit(单页条数)和 type(排序方式,通常在 1 和 3 两个值里选)。我一般把 limit 设成 50 到 100,太大接口容易吞请求。headers 至少要伪装成一个看起来正常的浏览器会话:User-Agent 直接复制你自己浏览器的即可;Referer 填详情页地址;Cookie 在未登录状态通常也能拿到一部分数据,但如果要求高,就用已登录会话的 Cookie 兜底。
import requests import time import json def fetch_reviews(movie_id: int, offset: int, limit: int, headers: dict) -> list: # 实际接口地址从开发者工具里复制,别手打 url = "https://example.com/api/comment" params = {"movieId": movie_id, "offset": offset, "limit": limit} for attempt in range(3): try: resp = requests.get(url, params=params, headers=headers, timeout=5) resp.raise_for_status() data = resp.json() return data.get("comments", []) except Exception as e: print(f"第 {attempt + 1} 次请求失败: {e}") time.sleep(2 ** attempt) # 退避策略,连续失败时指数休息 return []这段代码的逻辑很简单:请求失败不立刻放弃,而是按照 2 的指数次方秒数退避后重试,最多三轮。为什么要这个退避策略?因为猫眼这类站点对高频请求非常敏感,连续失败时你不休息,继续硬刚大概率触发风控。请求成功后,我建议先落盘存 JSON,不要立刻转 CSV。原始 JSON 保留了评论 ID、时间、点赞数等元数据,万一后面想做深度分析,重新取字段时你会感谢这个习惯。爬取循环里,每请求完一页至少 sleep 0.5 到 1 秒,数据量小,没必要追求极限速度。
2.2 评论清洗预处理:去重、去噪、保留情感标点
采集完的原始评论不会直接拿去分词,先过几步清洗。第一是去重,同一个用户多次短评,或者营销号复制粘贴,都会造成重复样本。轻量做法是对文本做归一化后算 MD5,重了就丢弃:全角转半角,去掉空白字符,再哈希比较。第二是去噪,具体操作是去掉 URL、@用户、HTML 实体和系统插入的话题占位符。这里有个关键原则:宁少勿多。
数据清洗里最容易翻车的是把带情感信息的符号一起删了。比如中文影评里“太好了!!!”三个感叹号本身是强烈正向信号,你把感叹号全清掉,模型就少了一个特征。再比如“特效强到没话说”这种表达,光靠字面很难识别,但标点和上下文还能提供一部分线索。我一般会保留感叹号和问号这两种标点,替换成特殊标记,让模型学到“情绪强烈”的隐含含义。常用做法是构造一个统一的清理函数。
import re import hashlib def normalize_text(text: str) -> str: # 全角转半角,去掉首尾空白,再做归一化 text = text.replace("\u3000", " ").strip().lower() return text def clean_review(raw: str) -> str: text = raw text = re.sub(r"https?://\S+", "", text) # 去 URL text = re.sub(r"@\S+", "", text) # 去 @用户 text = re.sub(r"<[^>]+>", "", text) # 去 HTML 标签 text = text.replace("&", "&").replace(" ", " ") # 解码实体 # 感叹号和问号是情感强度特征,替换成标记而不是删掉 text = text.replace("!!!", " !!! ").replace("!", " ! ") text = text.replace("??", " ??? ").replace("?", " ? ") text = re.sub(r"[ \t]+", " ", text).strip() return text def is_duplicate(normed: str, seen: set) -> bool: digest = hashlib.md5(normed.encode("utf-8")).hexdigest() if digest in seen: return True seen.add(digest) return False这里的核心点在两个替换语句:把感叹号和问号转成带空格的形式,后续 jieba 分词会把它当作独立 token 处理。后面构建词表时,可以检查这些标记是否保留,如果明显对预测有帮助就留着。我个人经验是,这类特殊标点在短评数据集里往往比某些高频词更有区分度。
去重之后要处理评论长度。短评大多在 10 到 50 个中文字符之间,但也会混入“哈哈”“不错”这种极短评论。不要直接扔,因为“哈哈”可能是正面情绪。反而是那些单字评论,比如“烂”,要特别处理,建议加进词典而不是简单过滤。
2.3 中文分词:jieba 用户词典、停用词表与词性过滤
清洗完的文本,下一步是分词。英文自然语言用空格切开就行,中文必须过一个分词器,jieba 是绕不开的开源选择,生态成熟,自定义词典也方便。两个地方最容易踩坑:一是默认词典不覆盖《流浪地球》里的电影专名,“流浪地球”可能被切错,“MOSS”变成英文单词,甚至“刘启”和“刘培强”这两个角色名都可能被切得七零八落;二是停用词表如果过度删除,把含否定意义的“不”“没”“太”去掉,情感极性直接反转。
分词的稳定实践是:每次跑新语料之前先加载一个用户词典,把片名、导演、主演、关键道具和网络用语全部写进去。然后停用词表只删那些确实无语义功能的词,比如“的”“了”“啊”“呀”“就是”“一个”等;否定词、程度副词一个都不能删,它们是情感分类的主心骨。最后可以做词性过滤,保留动词、形容词、名词,副词看情况保留。
import jieba import jieba.posseg as pseg jieba.load_userdict("movie_userdict.txt") # 每行一个词,可带词频和词性 # 停用词表里永远不要删以下这类词:不、没、太、很、反正、居然 stop_words = set() with open("stopwords_cn.txt", encoding="utf-8") as f: for line in f: w = line.strip() if w in {"不", "没", "太", "很", "反正", "挺"}: continue stop_words.add(w) NEG_WORDS = {"不", "没", "太", "很", "挺", "反"} def tokenize_with_pos(text: str) -> list: result = [] for word, flag in pseg.cut(text): w = word.strip() if not w: continue if w in stop_words and w not in NEG_WORDS: continue # 标点只保留感叹号和问号标记 if w in {"!", "?"}: result.append(w) continue if flag in {"x", "w", "t", "f"} and w not in {"!", "?"}: continue result.append(w) return result这段代码做三件事:主体过滤靠停用词表;其次把感叹号和问号标记成情感强度 token;第三排除部分词性。跑完必须检查分词结果,不能盲目扔给模型。常见做法是随机抽 200 条人工看一下,确认“流浪地球”有没有被整体保留、“不”有没有被误删。我还额外关注含“但”的句子,“特效不错但剧情拉胯”这种转折结构,分完词必须保留“但”,它是情感极性的转折点。
3. 词向量嵌入与序列构造:把评论变成 RNN 能接收的定长输入
3.1 自训 Word2Vec 还是加载预训练词向量
分词之后,你得到的是字符串序列,模型不能直接吃字符串,必须转成词向量。这里有一个选择:自己用 gensim 训练 Word2Vec,还是直接加载中文预训练词向量。两者各有取舍,我来说实际场景下的判断逻辑。
预训练词向量覆盖词面广,一般有十几万个词,像“吴京”“刘慈欣”这种专名通常也有对应向量。但它有两个问题:一是词表来自新闻、百科类语料,和电影短评的口语表达有一定分布差异;二是模型训练时你很难靠预训练向量的空间把“烂片”“吹爆”这类网络用语真正刻进语义关系里。所以在这个项目里,我的推荐做法是:用语料自训练 Word2Vec,能保证电影的领域词汇和网络用语都进入词表。常见做法是拿预训练向量做随机初始化,再用自己语料微调,但这个任务自训往往已经够用。
实际操作时,先把分好词的列表全部汇总成句子列表,然后交给 Word2Vec。gensim 的接口很稳定,但参数要刻意设置,下一小节展开。
3.2 三个必调参数:embedding_dim、window 与 min_count
挑三个参数按影评短文本场景展开。embedding_dim 就是词向量的维度,短评这种轻量任务 128 已经够用,上到 256 反而容易过拟合,尤其是当你的数据集只有几千条时。window 指上下文窗口大小,短评平均句长短,我一般取 5,窗口再大会把同一句话里隔得太远的词关联起来,引入噪声。min_count 是词频阈值,在语料里出现次数少于这个值的词不进入词表,统一当作 unknown 处理。对于这个任务,min_count=3 是安全的,能压掉拼写噪声,又不至于把低频但关键的网络词弄丢。
通用训练代码示例:
from gensim.models import Word2Vec # all_seg_lists 是所有评论分词后的二维列表 model_w2v = Word2Vec( sentences=all_seg_lists, vector_size=128, window=5, min_count=3, sg=1, # skip-gram,小语料下比 CBOW 更容易学出区分度 epochs=10, # 短文本语料 10 轮足够,别贪 workers=4 ) # 训练完顺手抽几个词检查语义质量 for w in ["特效", "剧情", "好看", "烂片"]: if w in model_w2v.wv: print(w, [x[0] for x in model_w2v.wv.most_similar(w, topn=3)])代码解释:sg=1 选用 skip-gram 是因为语料规模小,它对低频词的向量质量更友好;如果语料大到几十万条,CBOW 训练更快。epochs 设置在 10 轮左右,短文本重复出现次数高,练太久反而让模型过拟合到具体搭配。窗口 5 保留前后 5 个词的共现关系,对影评这种短文本足够了。
词向量训练完别急着送进模型,先跑一个相似词冒烟测试。这是在打开黑匣子前做检查,看“特效”的最近邻是不是“画面”“视觉”“音效”。如果最近邻全是无关词,说明分词质量或训练参数有问题,先回头调,不要继续往下走。这一步在我做过的项目里能拦下将近三成的“翻车”。
3.3 定长填充与截断:max_len 怎么选,什么时候用 pack
RNN 的训练单位是 batch,batch 内每条评论长度必须对齐。我一般先统计长度分布,用 99 分位数当 max_len。短评场景常见 max_len 在 120 左右,高于 200 就过头了。为什么要选 99 分位而不是最大值?因为个别刷屏长评的条数极少,却会把序列撑到几千,全体跟着它对齐,计算量浪费,损失更大的是大量样本被 padding 稀释掉。
PyTorch 里构造定长序列:
from torch.nn.utils.rnn import pad_sequence def make_sequence(word_ids: list, max_len: int): if len(word_ids) > max_len: word_ids = word_ids[:max_len] return word_ids sequences = [make_sequence(wids, max_len) for wids in all_word_ids] padded = pad_sequence( [torch.tensor(seq, dtype=torch.long) for seq in sequences], batch_first=True, padding_value=0 ) # padded 形状: [batch_size, max_len]关键点:padding_value=0,词表里 0 号 token 永远留给 ,不参与梯度更新。训练时可以用 pack_padded_sequence 把真实长度计算量压下来,避免 RNN 在 padding 部分做无用功:
from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence packed_seq = pack_padded_sequence( emb, lengths, batch_first=True, enforce_sorted=False ) output, hidden = rnn(packed_seq) output, _ = pad_packed_sequence(output, batch_first=True)这里有个实现差异。Keras 里 GRU/LSTM 层自带 mask 机制,自动跳过 padding;PyTorch 里可以用 pack 处理跳过。我从一开始就推荐用 pack_padded_sequence,习惯成自然,后面做长序列预测时不用回头改结构。
4. RNN 模型训练与评估:从模型结构到三分类收敛
4.1 模型结构:Embedding + BiGRU + 全连接的三分类设计
很多教程一上来就堆 LSTM,而我会从 BiGRU 开始。GRU 比 LSTM 少一个门,参数更少,短文本任务上和 LSTM 精度基本持平,训练速度更快,过拟合风险也更低。输入层是 nn.Embedding,中间是双向 GRU,最后接一个全连接层做三分类。为什么用双向而不是单向?因为影评里一句“前半段一般,后半段炸裂”的情感极性强依赖后半段内容,单向 RNN 读到结尾时,前半段信息已经被长程遗忘压得很弱,双向结构可以让模型在任意时间步同时看到前后的上下文。
import torch import torch.nn as nn class BiGRUClassifier(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim, num_classes=3, dropout=0.4): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0) self.rnn = nn.GRU(embed_dim, hidden_dim // 2, bidirectional=True, batch_first=True, num_layers=1) self.dropout = nn.Dropout(dropout) self.fc = nn.Linear(hidden_dim, num_classes) def forward(self, x): emb = self.embedding(x) # [B, T, E] out, hidden = self.rnn(emb) # out: [B, T, hidden] # 取最后一个时间步的隐藏状态拼接。注意双向的顺序 last_forward = out[:, -1, :hidden_dim // 2] last_backward = out[:, 0, hidden_dim // 2:] h = torch.cat((last_forward, last_backward), dim=-1) h = self.dropout(h) logits = self.fc(h) # [B, 3] return logits这段代码里 forward 取的是 out 而不是 hidden,原因在于双向 GRU 返回的 hidden 是两层双向状态的拼接,直接用 hidden 拼接容易把方向搞反,out 里则明确按时间步保存了方向信息。取最后一个前向状态和第一个反向状态拼起来,就是一个既能看见前文又能看见后文的顶点向量。
实现这里有个坑:双向 GRU 取最后一个时间步时,反向状态并不是最后一个时间步的输出,而是第一个时间步的输出。很多人在这里直接把 out[:, -1, :] 全拿过来用,等于同时取了正向最后一个和反向最后一个,方向信息全乱了。只要把 forward 里那两行记熟,用双向 RNN 基本不会再踩这个坑。
4.2 训练配置:learning_rate、batch_size、dropout 与早停
模型落到训练阶段,有四个参数需要刻意设,随便取默认值往往不收敛或过拟合。
第一个是 learning_rate。Adam 时代大家都默认给 1e-3,但我会看数据集规模。几千条短评,我一般从 2e-3 起步,跑 3 到 5 个 epoch 后观察损失曲线。稳定下降就继续,震荡就减半,降到 5e-4。第二个是 batch_size,短文本场景不要贪大,64 或 128 都可以。batch 太大模型更快见过整个数据集,容易过早收敛到次优解。第三个是 dropout。这个任务 0.4 起步,模型小可以降到 0.3。千万不能开 0,否则模型在学习阶段过拟合到句子级搭配,验证损失很快就抬起来。第四个是早停 patience。我固定用 3 个 epoch 不下降就停,同时把验证集上表现最好的模型权重重新恢复。
训练管线代码大致如下:
import torch.optim as optim criterion = nn.CrossEntropyLoss() optimizer = optim.AdamW(model.parameters(), lr=2e-3, weight_decay=1e-4) scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode="min", patience=2) best_f1 = 0 patience = 0 for epoch in range(20): model.train() for batch_x, batch_y in train_loader: optimizer.zero_grad() logits = model(batch_x) loss = criterion(logits, batch_y) loss.backward() optimizer.step() model.eval() val_loss, val_pred, val_true = evaluate(model, val_loader) scheduler.step(val_loss) macro_f1 = f1_score(val_true, val_pred, average="macro") if macro_f1 > best_f1: best_f1 = macro_f1 torch.save(model.state_dict(), "best_model.pt") patience = 0 else: patience += 1 if patience >= 3: print(f"Early stop at epoch {epoch}") break要点说明:CrossEntropyLoss 在多分类里同时包含 softmax 和损失计算,所以你网络最后一层别再手动加 softmax,否则等于做了两次。额外加权衰减 1e-4,对影评短文本有实测帮助,能阻止部分高活跃词向量把自己学到极端。评分用宏平均 F1,具体原因下一小节展开。
4.3 评估指标:准确率只是入场券,F1 和混淆矩阵才见真章
三分类情感识别项目里,我最警惕的指标就是准确率。一个极端例子:如果语料里正面占 50%,中性占 30%,负面占 20%,你全部猜正面,准确率就有 50%。模型看起来没白练,但这种结果没有任何使用价值。尤其是情感分类天然不均衡,准确率虚高几乎是常态。
正确评估方式看三样东西:混淆矩阵、宏平均 F1、置信度分布。混淆矩阵能直观暴露模型把负面误判成中性的系统性行为,这是方向性线索;宏平均 F1 把三个类别的 F1 做算术平均,不会让小类别被大类别淹没。置信度分布则能看到模型的判断拿不准的区域,这部分是后续阈值调优的原材料。
from sklearn.metrics import confusion_matrix, classification_report # val_pred 是 N 个样本的标签,val_true 是真实标签 cm = confusion_matrix(val_true, val_pred) print(cm) print(classification_report(val_true, val_pred, target_names=["负面", "中性", "正面"]))看输出时,第一眼看主对角线上的数字,再盯一下负面-中性和中性-负面两个交叉项。电影短评里中性往往会和负面混淆,因为很多人用“平淡”“一般”“普通”表达没有明显夸也没有贬,模型如果都归到中性,说明它对负面强度的感知还没学到。
5. 避坑与排查:RNN 影评三分类最容易翻车的 4 个问题
5.1 数据不平衡,模型学成“全猜中性”
现象:训练完成后,测试集预测结果里中性占了大半,正面和负面几乎没有输出;分类报告里中性 F1 很高,另外两个类别惨不忍睹。
原因:短评数据正面一贯偏多,中性也不少,负面相对少。CrossEntropyLoss 对每个样本一视同仁,模型只要学会把多数类输出成中性,就能得到很低的平均损失,根本不需要去学负面特征。
解决:先按标签分布做 class weight,给少数类更高权重,是最直接的改法。PyTorch 里把 weights 传进 nn.CrossEntropyLoss(weight=weights)。如果加权后 F1 还是上不去,再对训练集做分层采样,确保每个 batch 内部三类数量都差不多。我用分层采样比较多,它比权重法更容易稳定收敛,代价是每个 epoch 要多做一步重排。
5.2 分词拆碎情感词,导致极性判断错误
现象:某条评论原文是“不烂,但也没吹得那么神”,分词结果变成“不 / 烂 / 但 / 也 / 没 / 吹得 / 那么 / 神”,模型预测成负面;而“烂”单独出现时也是负面,模型没有真正抓住否定结构。
原因:词典对“吹得”“神作”这类组合覆盖不足,把网络用语当普通词切开。更关键的是停用词表把“不”“没”当作停止词删掉,让原本的否定结构失效了。
解决:停用词表永远不要包含否定词和程度副词,我在 2.3 已经强调过。然后对领域专名和组合词写用户词典,“吹得”“神作”“烂片”“还行”这类评论高频词,全部在 userdict 里明确指定成整体。分词这一步做完后必须抽样看结果,这是唯一可靠的检查手段。
5.3 长影评梯度消失,模型只记住结尾情绪
现象:一条 400 字的影评,前文全是夸,结尾补了一句“但整体可以一看”,模型最后判成正面。人工看正文发现整段其实是负面评价,模型完全没抓住。
原因:RNN 的长期依赖能力有限,越长的序列,前面信息被遗忘得越干净。单取最后时间步的隐藏状态作为全连接输入时,丢掉的远不只是细节,而是整段中段信息。
解决:第一选择是把模型改成双向 GRU,前面 3.3 提到的 pack 处理能减少无效计算。第二是把 max_len 限制在 200 以内,超长评论截断到主干部分,让模型聚焦在核心情绪区间。第三种方案是加一层轻量注意力,对所有时间步输出做加权求和,弱化开头信息对尾部的不公平影响。对于这个任务,前两种足够,注意力会额外加参数,短文本里性价比未必高。
5.4 训练损失下降、验证损失上涨:过拟合的抢救顺序
现象:训练集 loss 从 0.9 稳步跌到 0.2,验证集却从 0.8 先降后涨到 1.2,F1 也随之回落。这时候有人开始调各种参数,各种玄学操作都上了。
原因:模型容量对几千条短评数据来说太大了。embedding 层本身就有十几万参数,训练过程中把每个词的向量都朝训练样本里的语境逼近,一旦遇到和训练样本分布不一致的词,推理时就全乱。
解决:我遵循固定抢救顺序,前面 4.2 里有提到。先开 dropout,给 embedding 出口的向量加 Dropout;再做 weight_decay,用 1e-4;如果验证损失继续涨,就降低 hidden_dim。以上都试过还是不行,就回到数据层面,准备更多标注数据。
6. 进阶技巧:用置信度阈值细化三分类判断
模型训练完,得到的是每个样本在三个类别上的 softmax 概率。很多人直接取 argmax 当最终标签,我在实际项目里通常把这一步换成两阶段判断:第一阶段用 argmax 拿到标签;第二阶段再看这个样本的置信度,低于 0.6 的就归入不确定区,拿回人工复核或默认归中性。
这背后的逻辑是:三分类里置信度低本身是有业务价值的信号。比如平台要做差评预警,模型把 0.6 概率的中性和 0.95 概率的中性一样看待,最终会漏掉那些看起来最像差评但还没完全踩线的用户。我在拿猫眼影评数据分析时,会把负面类概率大于 0.7 的评论拿出来人工复核,效果比只看 argmax 精确得多。
如果想让阈值调整更系统化,可以对 logits 做一次温度缩放。温度 T 是一个标量,在 softmax 之前把 logits 除以 T,T 大于 1 时概率分布被拉平,模型更保守;T 小于 1 时更尖锐,模型更自信。训练完在验证集上扫几个 T 值,看 F1 曲线的峰值点。这个手段像开盲盒,但意外有效,尤其是对中性类过度自信的问题。
def temperature_scale(logits: torch.Tensor, temperature: float) -> torch.Tensor: # T=1 就是原始模型;T<1 更尖锐,T>1 更平缓 return (logits / temperature).softmax(dim=-1)我做过的项目里,T 在 0.8 到 1.2 之间扫一遍,宏观 F1 一般能有 1 到 3 个百分点的提升,代价几乎为零。唯一要注意的是:温度缩放要在验证集上做,不能用训练集,否则就是把过拟合搬到了后处理层。
还有一个小技巧,比较冷门但好用:把接近边界让模型拿不准的样本单独拉出来检查特征词。如果模型在“特效不错剧情不行”上怎么都拿不准,把这类样本输入和特征权重打印出来,人工看一遍它为什么模糊。RNN 隐藏状态说到底是一个黑匣子,但让黑匣子输出一条可视化样例,比盯着 loss 曲线反复调参更实际。
我自己的经验教训是:别后半段一路追着 F1 跑,最终给业务看结果时,最关心的不是 0.02 的 F1 提升,而是阈值调整后误判到底减了多少。调阈值比调模型结构划算,先把这个便宜占到手,再考虑更复杂的模型。如果中途哪一步卡住了,回到清洗和分词环节复查一遍,比重新调参更有效。希望帮到你。
本文还有配套的精品资源,点击获取