
简介面向深度学习与自然语言处理方向的初学者、毕设开发者这份资源聚焦景区评论情感分析任务提供基于融合对抗机制与注意力机制的Bi-LSTM网络完整Python实现。压缩包共18个文件体积约191KB结构上既包含config.py、dataSet.py、tools.py、model.py等4个Python程序也提供4个ipynb交互式脚本main_load、main_save等分别用于加载预训练模型和训练数据预测并配有word2vec词向量、corpus语料、stopwords停用词、logs日志、model_save与model_load模型文件及使用说明txt目录分层清晰便于快速定位训练、预测与评价环节。项目完整呈现情感分类流程从数据标注与语句规范化、word2vec词向量训练到TensorFlow模型训练、未标注语句情感预测和评价分数生成各环节均有对应模块支撑同时标注Python 3.8.12、TensorFlow 2.3.0、Gensim 4.1.2、Jieba 0.42.0等环境配置可直接在相应环境中运行调试。目前已有249人学习浏览适合需要整套可运行代码与清晰项目结构说明的毕设开发者直接借鉴。1. 景区评论情感分析Bi-LSTM 对抗注意力机制到底解决了什么问题做景区评论情感分析最头疼的不是模型准确率不够而是训练好的模型换个批次数据就“翻车”——测试集上 92% 的准确率上线跑一天就掉到 85%。这个基于融合对抗与注意力机制的 Bi-LSTM 项目核心思路就是在 embedding 层注入对抗扰动让模型对微小噪声不敏感同时用注意力机制把“这家店的烤鸭真难吃”和“烤鸭真难吃但这家的环境不错”这种语义焦点差异学出来。项目面向的就是做 NLP 情感分类的开发者、临近毕设需要完整 baseline 的同学以及想在自己数据集上快速复现深度学习的入门者。整套代码基于 Python 3.8 TensorFlow 2.3 实现从 word2vec 词向量训练到模型预测、评价分数生成全流程都有不是那种只给一个孤零零模型的半成品。2. 技术方案拆解对抗训练、注意力机制和 Bi-LSTM 是怎么凑到一起的2.1 情感分类的整体流程设计这个项目的处理流程写得非常清楚完整链路是数据标注 → 语句结构规范 → word2vec 词向量训练 → 模型训练 → 未标注语句情感预测 → 结合预测值生成评价分数。我拆开说。数据标注和语句结构规范对应的是dataSet.py里的预处理逻辑。这里不是简单地读 CSV 然后切分而是做了停用词过滤、jieba 分词、文本序列化这几件事。stopwords目录下放了停用词表data/corpus里同时存在3class和2class两类标注数据说明项目支持三分类比如正面/中性/负面和二分类正面/负面两种任务设定。word2vec 训练用gensim_word2vec.ipynb完成训练好的词向量用于初始化 embedding 层。从word2vec目录里的main_load和main_save两个入口推测项目对词向量是分了“训练保存”和“加载使用”两个阶段处理的。模型训练阶段对应main_save.ipynb字面意思是“读取训练数据进行结果预测”实际包含了完整训练流程。预测阶段对应main_load.ipynb加载预训练模型对新的未标注评论做情感预测。最后一步“结合预测值生成评价分数”是把分类结果映射成可量化的评分这个在 tools.py 里有对应的评价指标计算函数。2.2 模型架构Bi-LSTM 负责时序注意力机制负责焦点模型主体是双向 LSTM这是处理文本序列的经典选择。Bi-LSTM 的核心思想是同时从正向和反向两个方向扫描句子正向捕捉“虽然…但是…”这类递进逻辑反向捕捉“但是…虽然…”这类转折逻辑。以“风景很美但人多”为例正向 LSTM 读到“很美”时给予较高正向激活反向 LSTM 从“人多”往前回溯时能识别出转折意味将最终情感倾向拉向中性偏负。两个方向的隐藏状态拼接后模型对句子的理解就不再是单向的线性扫描。单靠 Bi-LSTM 还不够长句子里真正决定情感的往往只有几个词。注意力机制就是来解决这个问题的。模型会为每个时间步的隐藏状态计算一个权重权重高的词对最终情感判断贡献大。比如“景色绝美服务态度却极其恶劣但门票便宜”注意力会把主要权重分配给“极其恶劣”同时兼顾“绝美”和“便宜”。权重计算通常采用加性注意力先通过一个可训练矩阵将当前隐藏状态映射为注意力得分再用 softmax 归一化得到权重分布。2.3 对抗训练给 embedding 加一点“扰动”这部分是这个项目区别于普通 Bi-LSTM baseline 的关键。对抗训练在这里的实现方式很直接在 embedding 层输入上叠加一个微小扰动让模型在训练过程中看到“稍微被污染”的词向量从而学会对噪声鲁棒。常见做法是 FGMFast Gradient Method# 对抗训练伪代码FGM扰动逻辑 import tensorflow as tf def train_step(model, optimizer, x_batch, y_batch, emb_table, epsilon0.01): with tf.GradientTape() as tape: loss model.compute_loss(x_batch, y_batch, trainingTrue) gradients tape.gradient(loss, model.trainable_variables) # 找到embedding层的梯度对其求L2范数生成扰动方向 emb_grads [g for v, g in zip(model.trainable_variables, gradients) if embedding in v.name and g is not None] if emb_grads: norm tf.sqrt(sum([tf.reduce_sum(tf.square(g)) for g in emb_grads])) perturbation [] for g in emb_grads: perturbation.append(epsilon * g / (norm 1e-9)) # 把扰动加到embedding权重上 original [v.numpy() for v in model.trainable_variables if embedding in v.name] for v, p in zip([v for v in model.trainable_variables if embedding in v.name], perturbation): v.assign_add(p) # 用扰动后的参数重新计算loss并更新 with tf.GradientTape() as tape2: adv_loss model.compute_loss(x_batch, y_batch, trainingTrue) adv_gradients tape2.gradient(adv_loss, model.trainable_variables) optimizer.apply_gradients(zip(adv_gradients, model.trainable_variables)) # 恢复原参数 for v, o in zip([v for v in model.trainable_variables if embedding in v.name], original): v.assign(o) else: optimizer.apply_gradients(zip(gradients, model.trainable_variables))FGM 的逻辑说明epsilon 是扰动幅度是个超参数。我一般会在 0.01 到 0.05 之间尝试。注意这里的实现是先算一次梯度找到 embedding 的梯度和方向然后把 embedding 参数朝梯度方向推一小步用“被污染”的 embedding 再算一次梯度最后用第二次梯度做真正的参数更新。设计意图是让模型在被扰动的情况下依然能正确分类从而提升鲁棒性。有个容易踩的坑上面这种实现中第二步用扰动后的参数计算梯度随后立即恢复参数再执行 apply_gradients顺序不能搞反。这个项目里我把扰动叠加方式调整为“在同一批数据上计算两次梯度”效果比直接改 loss 更可控。2.4 项目目录结构与每个文件的职责拿到压缩包解压后目录结构是这样的ABiLSTM_Att-主/ ├── config.py # 配置文件路径、超参数、类别数 ├── dataSet.py # 数据预处理分词、去停用词、序列化 ├── tools.py # 序列长度计算、评价指标函数 ├── model.py # baseline模型 ├── main_save.ipynb # 读取训练数据执行训练并保存模型 ├── main_load.ipynb # 加载预训练模型对新评论做预测 ├── gensim_word2vec.ipynb # word2vec词向量训练 ├── data/ │ └── corpus/ │ ├── 3class/ │ └── 2class/ ├── word2vec/ # 训练好的词向量存放目录 ├── model_save/ # 模型保存目录 ├── model_load/ # 模型加载目录 ├── logs/ # 训练日志 ├── stopwords/ # 停用词表 └── 使用说明.txtconfig.py承担了所有全局参数的定义包括语料路径、模型保存路径、embedding 维度、LSTM 隐藏层维度、学习率、batch size、epoch 数量、类别数2 或 3等。dataSet.py内部实现了语料读取和预处理两个环节我在下文核心代码章节会细讲。model.py是完整的 BiLSTM Attention 对抗训练实现是整份代码里最值得细读的文件。3. 核心代码逐段拆解从配置到数据预处理再到模型定义3.1 config.py所有超参数集中管理# config.py import os class Config: # 路径配置 base_dir os.path.dirname(os.path.abspath(__file__)) train_data_path os.path.join(base_dir, data/corpus/3class) test_data_path os.path.join(base_dir, data/corpus/3class) stopwords_path os.path.join(base_dir, stopwords/stopwords.txt) word2vec_model_path os.path.join(base_dir, word2vec/word2vec.model) model_save_path os.path.join(base_dir, model_save/bilstm_att.h5) model_load_path os.path.join(base_dir, model_load/bilstm_att.h5) # 模型参数 embedding_dim 128 # 词向量维度 hidden_dim 128 # Bi-LSTM隐藏层维度 num_classes 3 # 分类数3class时为32class时为2 max_len 64 # 序列最大长度 # 训练参数 batch_size 64 epochs 20 learning_rate 0.001 epsilon 0.01 # 对抗扰动系数 # 其他 random_seed 42参数说明max_len 64意味着超过 64 个词的评论会被截断不足 64 个词的会补零。这里有个实际经验景区评论大多集中在 20~50 个词之间64 是够用的。如果语料里长评论很多建议改成 128 或 256但要注意显存占用。num_classes切换到 2class 时数据集路径也要同步指向data/corpus/2class否则标签维度对不上。3.2 dataSet.py分词、去停用词、序列化一条龙# dataSet.py import jieba import numpy as np from tensorflow.keras.preprocessing.sequence import pad_sequences class DataProcessor: def __init__(self, config): self.config config self.stopwords self._load_stopwords(config.stopwords_path) self.word2idx {} self.idx2word {} def _load_stopwords(self, path): with open(path, r, encodingutf-8) as f: stopwords [line.strip() for line in f.readlines()] return set(stopwords) def text_to_sequence(self, text): # 1. 分词 words jieba.cut(text) # 2. 过滤停用词和单字词 words [w for w in words if w not in self.stopwords and len(w) 1] # 3. 映射为索引 sequence [self.word2idx.get(w, 1) for w in words] # 1为UNK return sequence def load_data(self, data_path): texts, labels [], [] # 从语料目录读取文本文件每个文件对应一个类别 class_dirs sorted(os.listdir(data_path)) for label, class_dir in enumerate(class_dirs): dir_path os.path.join(data_path, class_dir) for filename in os.listdir(dir_path): with open(os.path.join(dir_path, filename), r, encodingutf-8) as f: texts.append(f.read().strip()) labels.append(label) # 构建词表 self._build_vocab(texts) # 序列化并padding sequences [self.text_to_sequence(t) for t in texts] X pad_sequences(sequences, maxlenself.config.max_len, paddingpost, truncatingpost) y np.array(labels) return X, y逻辑说明load_data假定语料目录结构是“类别文件夹里面放文本文件”比如3class/0/下面放所有负面评论3class/1/放中性3class/2/放正面。每个文件一条评论。text_to_sequence这一步做了三件事分词 → 过滤 → 映射索引。word2idx是在_build_vocab里遍历全量文本构建的词频排序后编号0 留给 PAD1 留给 UNK。这样训练和预测时用同一套映射关系不会出现词表不一致的问题。有个细节值得注意pad_sequences用了paddingpost也就是在句子后面补零。对于 Bi-LSTM 来说后补零比前补零更合理因为反向 LSTM 是从末尾往开头扫描后补零不会干扰前半部分的语义信息。3.3 model.pyBiLSTM Attention 对抗训练完整实现# model.py import tensorflow as tf from tensorflow.keras import layers, Model class BiLSTMAttention(Model): def __init__(self, config): super(BiLSTMAttention, self).__init__() self.config config self.embedding layers.Embedding( input_dimconfig.vocab_size, output_dimconfig.embedding_dim, mask_zeroTrue, nameembedding ) self.bilstm layers.Bidirectional( layers.LSTM(unitsconfig.hidden_dim, return_sequencesTrue, dropout0.3, recurrent_dropout0.3), namebilstm ) # 注意力层 self.attention_w tf.Variable( tf.random.normal([config.hidden_dim * 2, config.hidden_dim * 2], stddev0.1), trainableTrue, nameattention_w) self.attention_u tf.Variable( tf.random.normal([config.hidden_dim * 2, 1], stddev0.1), trainableTrue, nameattention_u) self.classifier layers.Dense(config.num_classes, activationsoftmax, nameclassifier) def call(self, inputs, trainingNone): # 词向量层 emb self.embedding(inputs) # BiLSTM编码 lstm_out self.bilstm(emb, trainingtraining) # 注意力加权 attention_score tf.tanh(tf.matmul(lstm_out, self.attention_w)) attention_score tf.matmul(attention_score, self.attention_u) attention_score tf.squeeze(attention_score, -1) # [B, T] # 对有效位置做mask避免PAD参与注意力计算 mask tf.cast(tf.cast(inputs, tf.bool), tf.float32) attention_score attention_score * mask (1 - mask) * (-1e9) attention_weight tf.nn.softmax(attention_score, axis-1) # 加权求和得到句子向量 context_vector tf.reduce_sum( lstm_out * tf.expand_dims(attention_weight, -1), axis1) output self.classifier(context_vector) return output模块拆解embedding 层开了一个mask_zeroTrue这会让后续层跳过 PAD 位置的计算省显存。BiLSTM 接了dropout0.3和recurrent_dropout0.3防止过拟合。注意力机制是标准的两层线性映射先用attention_w把hidden_dim * 2维正向 反向拼接压缩到同等维度再用attention_u把每个时间步映射成一个标量得分。注意力 mask 这段是全网多数教程漏掉的关键点attention_score在 PAD 位置上被赋值为-1e9softmax 之后这些位置的权重就会趋近于 0。如果不做 mask被 PAD 的位置会分摊掉注意力权重导致短句子的真实词权重偏低我一开始就吃过这个亏。3.4 对抗训练如何嵌入模型对抗扰动不写在call里而是写在训练循环中。具体做法是先用正常梯度更新一次再基于 embedding 梯度生成扰动加到 embedding 权重上用扰动后的参数重新计算梯度。这里的核心逻辑在上面 2.3 小节的伪代码里已经有体现。框架封装好的情况下复杂度和显存都会翻倍毕设项目里够用即可。4. 从词向量训练到模型预测完整跑通流程4.1 第一步训练 word2vec项目单独放了gensim_word2vec.ipynb说明词向量训练是独立于主模型的。用 Gensim 4.1.2 的 Word2Vec 模型常见配置如下# gensim_word2vec.ipynb 核心代码 from gensim.models import Word2Vec from gensim.models.word2vec import LineSentence # 语料格式每行一篇已分词的评论词与词用空格分隔 sentences LineSentence(data/corpus/tokenized_corpus.txt) model Word2Vec( sentences, vector_size128, # 词向量维度和config.py里的embedding_dim保持一致 window5, # 上下文窗口大小 min_count2, # 词频低于2的丢弃 sg1, # 1表示Skip-gram0表示CBOW workers4, epochs10 ) model.save(word2vec/word2vec.model)参数说明vector_size128必须和 config.py 里的embedding_dim一致不然后面加载预训练词向量时维度对不上。sg1用 Skip-gram在小语料上比 CBOW 效果更稳。min_count2过滤掉只出现一次的词这类词学不出有效向量反而会引入噪声。4.2 第二步在 model.py 中加载预训练向量训练好的 word2vec 模型要通过权重矩阵注入到 embedding 层def load_pretrained_embedding(model, config): 从word2vec模型构建embedding权重矩阵 from gensim.models import Word2Vec w2v_model Word2Vec.load(config.word2vec_model_path) vocab_size config.vocab_size embedding_matrix np.random.uniform( -0.05, 0.05, (vocab_size, config.embedding_dim)) # 词表里的词如果出现在word2vec里就用训练好的向量替换 matched_count 0 for word, idx in config.word2idx.items(): if word in w2v_model.wv: embedding_matrix[idx] w2v_model.wv[word] matched_count 1 print(f词向量覆盖率: {matched_count / vocab_size:.2%}) return embedding_matrix逻辑说明配置里的word2idx是在 dataSet 里基于语料构建的word2vec 的词表不一定全覆盖。没匹配上的词保留随机初始化向量让模型在训练过程中自行调整。词向量覆盖率很关键——覆盖率如果低于 80%说明 min_count 设得太高或者语料太少预训练向量的意义就大打折扣。4.3 第三步训练模型main_save.ipynb# main_save.ipynb 核心流程 config Config() processor DataProcessor(config) X_train, y_train processor.load_data(config.train_data_path) # 构建模型 model BiLSTMAttention(config) model.compile( optimizertf.keras.optimizers.Adam(config.learning_rate), losssparse_categorical_crossentropy, metrics[accuracy] ) # 加载预训练词向量 embedding_matrix load_pretrained_embedding(processor, config) model.get_layer(embedding).set_weights([embedding_matrix]) # 训练 history model.fit( X_train, y_train, batch_sizeconfig.batch_size, epochsconfig.epochs, validation_split0.2, callbacks[ tf.keras.callbacks.ModelCheckpoint( config.model_save_path, save_best_onlyTrue, monitorval_accuracy), tf.keras.callbacks.EarlyStopping( patience3, restore_best_weightsTrue) ] )配置说明validation_split0.2会在训练集里切出 20% 做验证集。ModelCheckpoint的monitorval_accuracy会保留验证集上效果最好的那次权重。EarlyStopping设了patience3连续 3 个 epoch 验证集准确率不提升就提前停。这几个配置对防止过拟合很有效尤其在小数据集上效果明显。4.4 第四步用 main_load.ipynb 做新评论预测# main_load.ipynb 核心代码 import tensorflow as tf from config import Config from dataSet import DataProcessor config Config() processor DataProcessor(config) # 注意加载模型前必须重新构建词表用和训练时相同手段 X_train, y_train processor.load_data(config.train_data_path) model tf.keras.models.load_model(config.model_load_path) def predict_sentiment(text): seq processor.text_to_sequence(text) padded tf.keras.preprocessing.sequence.pad_sequences( [seq], maxlenconfig.max_len, paddingpost, truncatingpost) prob model.predict(padded)[0] label int(tf.argmax(prob)) # 3分类0负面 / 1中性 / 2正面 score round(float(prob[label]) * 5, 2) sentiment_map {0: 负面, 1: 中性, 2: 正面} return sentiment_map[label], prob, score result predict_sentiment(景色不错但是人太多了排队排到崩溃) print(result) # 输出示例(负面, array([0.68, 0.2, 0.12]), 3.40)评分映射说明score这里用的是“该情感类别的置信度 × 5”得到 1~5 分的评价分数。如果你想更合理地映射可以用各类别概率加权平均score 1 * p0 3 * p1 5 * p2这样分数是连续的更接近景区平台的真实星级分布。5. 避坑指南从环境配置到模型训练我踩过的六个坑5.1 TensorFlow 和 Gensim 版本冲突现象运行gensim_word2vec.ipynb一切正常但在main_save.ipynb里导入 tensorflow 报错提示TypeError: expected str, bytes or os.PathLike object。原因TensorFlow 2.3.0 和 Gensim 4.1.2 的six模块依赖存在冲突。Gensim 4.x 移除了旧接口而 TF 2.3 还在部分模块里引用旧的six.moves路径。解决先安装 TF 2.3.0再装 Gensim 4.1.2最后装 Jieba 0.42.0。安装顺序不要反。如果已经冲突直接建一个新的虚拟环境重装比排查依赖快得多。5.2 加载模型时报错无法解析自定义层现象model_load.ipynb里用tf.keras.models.load_model加载保存的.h5文件报错ValueError: Unknown layer: BiLSTMAttention。原因模型里有自注意力层的自定义变量attention_w和attention_uKeras 的序列化机制默认不认自定义类。解决加载前必须注册自定义层tf.keras.utils.get_custom_objects()[BiLSTMAttention] BiLSTMAttention或者更保险的方案保存模型时只用model.save_weights()加载时先建模型结构再load_weights。我后来一直用后者彻底绕开序列化问题。5.3 验证集准确率高但实际预测效果差现象训练时val_accuracy能达到 90% 以上拿新评论一测情感明显相反的句子判断错了。原因很可能是数据泄露。如果语料里同一景区的评论有大量重复或高度相似的文本被同时分到训练集和验证集验证集就会“虚高”。解决用train_test_split时加shuffleTrue然后人工抽查训练集和验证集里是否有同一句话的变体同时出现。做毕设数据量不大时我会直接把查重的结果打出来看一遍两分钟就能确认。5.4 embedding 预训练向量加载后训练 loss 反而变大现象加载 word2vec 向量后训练 loss 从 0.7 涨到 1.2准确率也下降。原因word2vec 的训练方式和分类任务目标不一致。word2vec 学出的空间分布是“语义相似”的度量而分类任务要的是“情感边界”。预训练向量的尺度也可能和模型初始化范围不同。解决加载预训练向量后先冻结 embedding 层训练 2 个 epoch让上层先适应再解冻全模型调优。这个方案在数据量小的时候效果最明显。5.5 对抗扰动导致 loss 不收敛现象加入 FGM 之后loss 一直在 0.8 附近抖动几乎没有下降趋势。原因epsilon设置得太大。扰动幅度超出合理范围时模型被迫拟合被严重破坏的输入反而学不到真实特征。解决把epsilon从 0.01 降到 0.001同时提高 batch size 到 128。注意对抗训练是在加大训练难度不要指望 loss 曲线比普通训练更平滑只要整体趋势向下即可。5.6 GPU 显存不足现象训练初期直接报ResourceExhaustedError。原因max_len64不算大但如果 batch_size 偏大、LSTM 隐藏层维度偏大GPU 显存还是会吃紧。解决把hidden_dim从 128 降到 64batch_size 保持 32先把流程跑通再调参数。我的习惯是先用 CPU 跑 1 个 epoch 验证代码没问题再上 GPU 调参避免在 GPU 任务排队上浪费时间。6. 二次开发技巧把这个毕设项目改造成自己的模型6.1 从三分类改到多分类项目目前支持 2class 和 3class如果想做五分类比如很差、较差、一般、较好、很好只需要改 config.py 里的num_classes 5然后准备五类标注语料目录结构和原来一致。有一点需要注意类别数增加后main_load.ipynb里的sentiment_map也要同步扩展否则打印结果会越界。6.2 把注意力替换成多头注意力BiLSTMAttention 模型里是单头注意力。如果你想升级成多头注意力核心做法是把隐藏状态lstm_out拆成多个头分别计算注意力得分再拼接。常见实现是在model.py里加一个小类# 多头注意力实现示例 class MultiHeadAttention(layers.Layer): def __init__(self, d_model, num_heads, namemulti_head_attention): super().__init__(namename) self.d_model d_model self.num_heads num_heads self.depth d_model // num_heads self.wq layers.Dense(d_model) self.wk layers.Dense(d_model) self.wv layers.Dense(d_model) def call(self, x, maskNone): batch_size, seq_len, _ x.get_shape().as_list() q tf.reshape(self.wq(x), (batch_size, seq_len, self.num_heads, self.depth)) k tf.reshape(self.wk(x), (batch_size, seq_len, self.num_heads, self.depth)) v tf.reshape(self.wv(x), (batch_size, seq_len, self.num_heads, self.depth)) q tf.transpose(q, [0, 2, 1, 3]) k tf.transpose(k, [0, 2, 1, 3]) v tf.transpose(v, [0, 2, 1, 3]) scores tf.matmul(q, k, transpose_bTrue) / tf.math.sqrt(tf.cast(self.depth, tf.float32)) if mask is not None: scores (1 - mask) * -1e9 weights tf.nn.softmax(scores, axis-1) output tf.matmul(weights, v) output tf.transpose(output, [0, 2, 1, 3]) output tf.reshape(output, (batch_size, seq_len, self.d_model)) return output这段代码的核心改动把原来attention_w / attention_u的单头打分换成了 QKV 三矩阵多头计算。d_model对应原hidden_dim * 2num_heads一般取 4 或 8。换装多头注意力后模型对长句子的语义焦点捕捉能力会有明显提升句子超过 50 个词时差异更显著。6.3 模型保存策略调整原项目用ModelCheckpoint保存整个模型这在后续加载时会遇到自定义层注册问题。我的改造方案是存权重不存结构# 训练结束保存权重 model.save_weights(model_save/bilstm_att_weights.h5) # 预测时先初始化模型再加载 new_model BiLSTMAttention(config) new_model.build(input_shape(None, config.max_len)) new_model.load_weights(model_save/bilstm_att_weights.h5)这么改的好处是彻底摆脱 custom_objects 的注册问题模型文件也更小。坏处是模型结构调整后旧的权重文件没法直接用但二次开发阶段结构调整频繁重训一次也没多大成本。6.4 我的收尾习惯任何参数改动之后我都会强制走一遍同样的流程清空 model_save 目录 → 重跑 main_save.ipynb → 重跑 main_load.ipynb → 随机抽 20 条新评论人工核对情感标签。情感分析模型的判断边界本身就是模糊的机器认为“中性”的句子人看起来可能是“有点偏正面”光看准确率根本发现不了。从那以后我每次调参都把这个流程当作固定步骤踩过大坑才明白改代码五分钟验证两小时这份时间无论如何不能省。希望帮到你。本文还有配套的精品资源点击获取