
前一阵帮朋友处理了一批某App的评论数据需求很简单判断每条评论的情绪。一开始我直接丢给一个二分类情感模型输出一堆“正向/负向”标签朋友追问了一句“负面到底是失望还是愤怒”我当场有点答不上来。后来我把大连理工大学的中文情感词汇本体库翻出来配合不到两百行Python代码把评论的情绪粒度从“正负二分类”提到了“七大类、二十一个小类的细分类”整个分析结果一下子变得能用了。这篇文章就围绕这个库的字段结构、数据清洗方式、完整情绪分析代码以及我实际跑数据时踩过的坑展开。适合正在做评论挖掘、舆情分析、客服质检、文本画像的同学也适合刚接触NLP、想从词典方法入门的初学者。我会尽量把每个环节的“为什么”讲清楚而不是只丢一段能跑但看不懂的代码。1. 这个库值钱在哪七大类情感的设计逻辑与字段说明1.1 为什么是七大类而不是简单的正负二分类做情感分析的都知道早期几乎所有国内外的情感资源都停留在“积极/消极/中性”这个粒度上。像SentiWordNet、通用的情感词典能告诉你一个词的好坏倾向但说不清“烦闷”和“悲伤”的区别。大连理工这个本体库的设计思路是参考了心理学中的情感分类理论在一级层面划分了七个维度乐、好、怒、哀、惧、恶、惊。这七个维度基本覆盖了中文里能被显性词汇表达的主流情绪。比如“愤怒”是一类“讨厌”“妒忌”“怀疑”“烦闷”在库中被归为“恶”这个大类而“慌”“恐惧”“羞”属于“惧”。这种分类在日常评论分析里特别实用——用户说“太坑了”你只知道是负面但具体是失望、愤怒还是烦闷决定了运营要采用完全不同的处理策略。二级分类更细。每个一级大类下还分若干小类例如“乐”分为“快乐 PA”和“安心 PE”“哀”分为“悲伤 NB、失望 NJ、内疚 NH、思念 PF”。小类用英文字母代码表示源代码里处理起来非常方便后面我会用一张映射表把这套代码转成中文标签。1.2 字段级拆解每个列都值得认真对待这个库常见的格式是Excel导成CSV之后每行是一个“词语-义项”级别的记录。核心字段如下字段类型含义示例词语string情感词本身高兴词性种类string该词在义项中的词性adj词义数int该词在库中有几个义项2词义序号int当前记录是第几个义项1情感分类string七个一级或二十一个二级类别代码PA强度int1/3/5/7/9数值越大情绪越强5极性int0中性、1褒义、2贬义1这里有个细节值得注意同一个词可能有多个义项每个义项的情感分类、强度、极性都可能不同。比如某个词既有中性义项又有褒义义项库中会分别占两行。这也是这个库比一般“词-极性”二元词典高级的地方它在词义层面做了消歧。所以我们读取数据时不能按词去重必须把“词语词义序号”作为唯一键来使用。强度字段用1、3、5、7、9这样的奇数刻度也很有讲究。奇数中间值为5方便区分“轻度情绪”和“强烈情绪”跨度拉大后计算情感得分时不同词之间的区分度也更明显不至于所有情感词都挤在同一档。极性字段则用于语义倾向的判断。我后续写代码时会把极性和强度结合起来正面情绪词给正分负面情绪词给负分强度作为权重这样就能算出一句话的整体倾向。2. 正式使用前的数据预处理把Excel词典变成可查询的结构2.1 不同格式的读取方式与编码处理我见过好几版大连理工情感本体库的发布格式常见的有以下三种格式打开方式注意点Excel.xls/.xlsxpandas.read_excel需要安装openpyxl或xlrdCSV逗号分隔csv模块或pandas注意编码可能是utf-8、gbk或gb2312TXT制表符分隔直接按\t切分查看表头时容易因为编码乱码我建议拿到数据后先观察第一行表头长什么样再决定怎么解析。如果是Excel格式直接转为CSV是更省事的做法——后续每次跑代码都不需要依赖pandas的Excel引擎。编码问题也容易踩坑。很多从学校资源站下载的版本Excel另存为CSV时系统会默认存成GBK或GB2312。用Python读取时如果直接指定utf-8会报UnicodeDecodeError。我一般这么处理先尝试utf-8失败就退回gbk。这个兼容逻辑直接写进工具函数里后面读任何词典都不会因为编码卡住。2.2 情感小类代码到中文标签的映射表原始库里的“情感分类”列是PA、NB、NJ这类字母代码直接读出来并不直观。我会维护一张映射表把21个小类映射到中文名和所属大类情感大类情感小类代码中文名情感大类情感小类代码中文名乐PA快乐惧NC慌乐PE安心惧NG恐惧好PD尊敬惧NH羞好PH赞扬恶NE憎恶好PG相信恶ND贬责好PB喜爱恶NN妒忌好PK祝愿恶NK怀疑怒NA愤怒恶NL烦闷哀NB悲伤惊PC惊奇哀NJ失望惊NI紧张哀NH内疚哀PF思念映射表的用途有两个一是可视化输出时直接显示中文标签二是做结果聚合时通过前缀字母把21个小类合并回7个大类。比如代码以N开头的大类情绪有怒、哀、惧、恶以P开头的大类有乐、好、惊。读入词典后我会把原始记录转成Python字典键是词语值是该词所有义项的情感信息列表。这样后续匹配时对每个分词结果只需要一次字典查询就能拿到全部候选情绪。3. 情绪分析核心代码从分词到七类情绪分布3.1 基础框架词典匹配型情绪分析器这个分析器的思路其实很朴素把待分析文本分词逐个词去词典里查若命中就读取它的情感分类、强度和极性累加到对应的情绪维度上。为了让代码更健壮我做了两个设计第一个设计是每个词维护一个“情绪向量”。向量长度设为7分别对应乐、好、怒、哀、惧、恶、惊。当某个词命中小类情绪时我不只在该小类上加分同时也把加权后的强度累加到它所属的大类上。这样既保留了细分类信息又不丢失大类别的判断能力。第二个设计是引入“否定词”和“程度副词”的修正机制。因为仅靠情感词本身判断很容易把“不满意”判成“满意”。文本里出现“不”“没”“别”这类否定词时需要把词的情绪极性反转出现“很”“太”“极其”这类程度副词时可以把强度加权放大。实现时我用一个简单的窗口扫描从情感词位置向前回溯看最近的否定词或程度副词是否出现。下面给出第一版基础代码只做词典匹配不考虑否定词和程度词import csv import jieba EMOTION_CODE_TO_LABEL { PA: (乐, 快乐), PE: (乐, 安心), PD: (好, 尊敬), PH: (好, 赞扬), PG: (好, 相信), PB: (好, 喜爱), PK: (好, 祝愿), NA: (怒, 愤怒), NB: (哀, 悲伤), NJ: (哀, 失望), NH: (哀, 内疚), PF: (哀, 思念), NC: (惧, 慌), NG: (惧, 恐惧), NH: (惧, 羞), NE: (恶, 憎恶), ND: (恶, 贬责), NN: (恶, 妒忌), NK: (恶, 怀疑), NL: (恶, 烦闷), PC: (惊, 惊奇), NI: (惊, 紧张), } def load_emotion_dict(csv_path): emot_dict {} with open(csv_path, r, encodingutf-8, errorsignore) as f: reader csv.DictReader(f) for row in reader: # 不同版本的表头名称可能不同做一层兼容 word row.get(词语) or row.get(word) category row.get(情感分类) or row.get(emotion) intensity row.get(强度) or row.get(intensity) polarity row.get(极性) or row.get(polarity) if not word or not category: continue try: intensity int(intensity) polarity int(polarity) except (ValueError, TypeError): intensity 1 polarity 0 # 用词语作为主键一个词可能映射到多个情绪记录 if word not in emot_dict: emot_dict[word] [] emot_dict[word].append({ category: category, intensity: intensity, polarity: polarity, }) return emot_dict def analyze_sentence_simple(text, emot_dict): # 七大类乐、好、怒、哀、惧、恶、惊 emotion_vector {乐: 0.0, 好: 0.0, 怒: 0.0, 哀: 0.0, 惧: 0.0, 恶: 0.0, 惊: 0.0} words jieba.lcut(text) for word in words: if word in emot_dict: for item in emot_dict[word]: # 由小类代码映射到大类名称 if item[category] in EMOTION_CODE_TO_LABEL: big_category, _ EMOTION_CODE_TO_LABEL[item[category]] # 强度作为权重累加 emotion_vector[big_category] item[intensity] return emotion_vector3.2 加入否定词和程度副词的完整版本基础版本的问题很明显“这个电影一点都不好看”这句话“好看”的褒义会被直接累加导致结果偏正面。因此必须做否定和程度修正。实现策略是维护两个辅助列表否定词表、程度副词权重表。在遍历分词结果时记录每个词在原始句子中的位置当命中的情感词是正面词且前面两个词范围内出现否定词时将其强度取反如果出现程度副词则将加权系数乘上强度值。我把程度副词的权重定义为一个字典例如“极其”权重1.8、“很”权重1.5、“有点”权重0.6、“比较”权重1.2。这些权重不需要特别精确行业里常用的人工设定思路是“强程度词大于1、弱程度词小于1”。如果后面有精力也可以用回归方式从标注语料里学这组权重但作为基线版本人工设定完全够用。完整代码NEGATION_WORDS set([不, 没, 无, 非, 莫, 勿, 别, 未, 不曾, 并非, 毫无]) DEGREE_WORDS {很: 1.5, 非常: 1.8, 极其: 1.8, 太: 1.6, 比较: 1.2, 有点: 0.6, 稍微: 0.6, 挺: 1.3, 特别: 1.7, 格外: 1.6} def analyze_sentence(text, emot_dict): emotion_vector {乐: 0.0, 好: 0.0, 怒: 0.0, 哀: 0.0, 惧: 0.0, 恶: 0.0, 惊: 0.0} detail_score {} words jieba.lcut(text) for idx, word in enumerate(words): if word in emot_dict: degree_weight 1.0 # 向前找否定词和程度副词 for back_idx in range(max(0, idx - 2), idx): prev_word words[back_idx] if prev_word in NEGATION_WORDS: degree_weight * -1 elif prev_word in DEGREE_WORDS: degree_weight * DEGREE_WORDS[prev_word] for item in emot_dict[word]: if item[category] not in EMOTION_CODE_TO_LABEL: continue big_category, small_category EMOTION_CODE_TO_LABEL[item[category]] # 极性修正1褒义/2贬义/0中性 if item[polarity] 2: weight -item[intensity] elif item[polarity] 1: weight item[intensity] else: weight item[intensity] * 0.2 # 中性词默认给很低权重 weighted weight * degree_weight emotion_vector[big_category] abs(weighted) if weighted 0 else 0 # 为了保留负向大类信息这里用另一套字典来记录净得分 detail_score.setdefault(big_category, 0.0) detail_score[big_category] weighted return emotion_vector, detail_score这里我特意用了两个返回值第一个emotion_vector是“情绪出现强度”的非负分布用于回答“这句话里哪类情绪感受最浓”第二个detail_score是“考虑褒贬极性后的净得分”用于回答“这句话整体偏正向还是负向”。二者结合既能看情绪类型也能看情感倾向。3.3 完整可运行示例读取CSV并分析一段文本把上面几个函数串起来就是一个可以直接跑通的脚本if __name__ __main__: emot_dict load_emotion_dict(emotion_dict.csv) test_text 这部电影真的太让人失望了剧情拖沓演员表演也很尴尬。 emotion_vec, detail_score analyze_sentence(test_text, emot_dict) print(情绪强度分布, emotion_vec) print(情绪净得分, detail_score) # 找出最强的情绪大类 strongest max(emotion_vec, keyemotion_vec.get) print(最强情绪, strongest)用这段代码跑上面那句话你会看到“哀”和“恶”两个大类分数明显偏高detail_score总体为负。这就比单纯输出“负面”二字要丰富得多它告诉你用户情绪里既有失望也有贬责和烦闷。4. 真正跑数据时你会遇到的几个坑4.1 编码与文件格式导致的读取失败这是新手问得最多的一个问题。下载到的是Excel代码按CSV读报错按CSV读出来了又乱码好不容易正常读出来发现字段名对不上。我的建议是无论原文件是什么格式先统一转成“UTF-8编码、逗号分隔、带表头”的CSV。转换时不要用Windows自带的记事本另存为因为它在部分系统版本上会写入BOM头。BOM头会导致pandas读取时第一列表头变成“\ufeff词语”这种奇怪的名字。正确处理方式是用Python转编码def convert_to_utf8(src_path, dst_path): raw open(src_path, rb).read() for enc in [gbk, utf-8, gb2312]: try: text raw.decode(enc) break except UnicodeDecodeError: continue with open(dst_path, w, encodingutf-8-sig) as f: f.write(text)转成utf-8-sig写入能保证Excel打开不乱码pandas读取时也能正确识别表头。4.2 多义词与词义消歧问题词典里同一个词有多个义项时如果不加处理所有义项的分数都会累加上去导致一个中性词被错误放大。比如“骄傲”一词既有“自豪”的褒义义项也有“自满”的贬义义项。如果文本是“我为祖国感到骄傲”按多个义项累加就会同时产生“好”和“恶”的分数结果变得很奇怪。遇到这种情况我的做法是引入“义项优先级”策略当同一个词命中多个情感条时优先取极性分数更大的那个或者简单取所有义项分数的均值。更精细的做法是利用词性信息——词性种类是adj还是verb可以作为消歧特征。不过老实说词典方法做完全消歧很难如果想精确处理多义词建议把词典作为特征投给深度学习模型而不是让词典独立做最终判决。4.3 词典覆盖不足与新词、网络热词大连理工这个库虽然覆盖了不少通用情感词但面对短视频评论、网络热梗、谐音梗时覆盖能力会明显不够。比如“破防了”“绷不住”“6”这类近年流行表达原版词典里大概率没有。我通常会在加载基础词典后再准备一个“自定义扩展词典”文件格式与主词典保持一致。跑数据之前把扩展词典合并进主词典这样既能保留本体库的标准化优势又能适应自己的业务场景。扩展词的情感分类、强度、极性自己标注即可标注规则参照原库强度从1到9取奇数极性按情感倾向填0、1、2。4.4 强弱的累加逻辑对结果影响不小我在3.2节里写的是直接把强度相加。但实际分析长文本时同一句话里重复出现多个情感词会快速提高某个情绪维度的得分这并不总是好事。比如“悲惨、可怜、痛苦”三个词同时出现在一句话中情绪强度确实高但如果拿这个向量做跨文本比较会出现长度越长得分越高的偏差。更好的方式是把累加结果除以句子中情感词的数量或句子总词数得到“单位强度”。如果做文档级情绪分析我建议在汇总时对每个情绪维度做归一化某个维度的得分除以该文档所有情绪维度得分之和得到该情绪的占比。这样不同长度的文本之间才具备可比性。5. 把词典玩出花与深度学习模型结合的正确姿势5.1 把七类情绪概率做成特征向量纯词典方法虽然简单但它的优势是稳定、可解释、不需要训练数据。工程上最实用的做法不是让词典和深度学习模型二选一而是把词典输出作为特征喂给下游模型。具体做法是对每条文本用词典分析得到7个情绪大类的强度占比或归一化得分拼成一个7维向量再把这个向量与文本的词向量表示拼接作为分类器输入。这样模型既保留了上下文语义信息又获得了词典提供的情绪先验知识。实测在评论情感多分类任务里这种“词典特征预训练模型”的组合比单独用模型要高2到3个百分点的准确率尤其是处理“隐含情绪词”的长文本时提升更明显。5.2 与预训练模型结合的一种轻量实现思路先说明如果你手里有大量标注数据直接微调BERT等预训练模型通常效果更好词典的作用会变小。但如果你处在“少量标注数据、业务快速上线”的阶段我推荐一个轻量方案用词典对每条文本生成七个情绪维度的强度分布写成一个数组。用预训练模型对文本取句向量一般是768维或1024维。将句向量和七维情绪向量拼接输入一个浅层全连接分类器输出业务需要的类别。这样做的核心理由是预训练模型擅长理解上下文但容易忽略显性情绪词词典恰好擅长捕捉显性情绪词。两者互补实现成本极低不需要重新训练大模型一台普通CPU机器都能跑推理。如果你连浅层分类器都嫌麻烦还有一个更朴素的操作直接用词典输出做规则判定。比如某条评论如果“怒”类分数最高且净得分为负就标记为“投诉风险”如果“哀”类分数最高就标记为“失望退订风险”。这个方案在客服工单分流场景非常好用因为不用训练任何模型逻辑完全可控。最后说一点我自己的体会。做情绪分析这些年我见过太多人一上来就上大模型反而忽略了像大连理工中文情感词汇本体库这样高质量的基础资源。好的词典基线跑通之后你不仅能用它快速交付业务方案还能为更复杂的模型提供特征和纠错依据。如果你手头正好有评论、问卷、客服对话这类文本数据不妨先试着把这个库用起来你会发现很多问题不一定要用深度学习才能解决。