ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

景区评论情感分析实战:Bi-LSTM融合注意力与对抗训练的Python实现

景区评论情感分析实战:Bi-LSTM融合注意力与对抗训练的Python实现

简介:面向景区评论情感分析场景的 Python 源码项目,以融合对抗训练与注意力机制的双向长短期记忆(Bi-LSTM)网络为核心,完整覆盖评论文本预处理、情感分类、词向量训练、模型预测与评价分数生成等环节,适合自然语言处理方向的本科生毕业设计,也适合深度学习初学者进行项目实践。压缩包共 18 个文件,包含 4 个 Jupyter Notebook 交互式脚本、4 个 Python 模块、预训练 Word2Vec 词向量、语料库、停用词表、日志与结果目录等,整体仅 191KB,结构紧凑、分层清晰,便于快速部署与调试。目前已有 249 人学习下载。代码按 config 配置、dataSet 数据预处理、tools 工具函数、model 基线模型等模块拆分,并分别提供 main_load 与 main_save 两个预测入口;项目说明明确列出 Python 3.8.12、TensorFlow 2.3.0、Gensim 4.1.2 等依赖环境,使用者可直接复现从数据标注、规范语句结构到生成评价分数的完整流程,同时也能基于现有模块替换自己的评论语料,开展对抗样本验证或注意力可视化等拓展实验。

1. 景区评论情感分析:这份Bi-LSTM融合注意力源码按什么逻辑落地

景区评论看起来只是几句话,但真正做情感分析时,你会发现最大的麻烦不是“怎么把好评差评分出来”,而是“怎么让模型在短文本里抓住真正的情绪”。这份以融合对抗与注意力机制的Bi-LSTM网络为核心的Python毕设源码,恰好把这条链路完整走了一遍:word2vec词向量训练、Bi-LSTM序列编码、注意力加权、对抗训练增强,再到预测结果直接映射成评价分数。我拆完后的感受是,它并不追求网络结构上的花哨,而是把工程环节收得很干净,适合正在做景区评论情感分析、又不想从零搭建pipeline的人直接对照复现。新手能顺着notebook跑通全流程,熟手也能直接改config和model.py做交叉实验。拿到包后建议先别急着跑,把目录结构和两个数据版本(3class、2class)的加载逻辑看一遍,再决定从哪个入口入手。

2. 模型原理与选型:word2vec、Bi-LSTM、注意力与对抗训练为什么这样组合

2.1 情感分类基线:词向量表示加序列编码再加分类头

一份景区评论,比如“风景很美但排队太久了”,机器拿到的是字符串。要做情感分析,第一件事是把它变成能进神经网络的数字表示。常见路线是先分词,再用word2vec把每个词映射为稠密向量,然后把整条评论的向量序列交给序列模型编码,最后接softmax分类头输出各情感类别的概率。这套表示-编码-分类的三段式结构,是这类任务的基线。

项目里没有直接用别人训练好的词向量,而是单独放了一个gensim_word2vec.ipynb,在自己的景区评论语料上重新训练word2vec。好处很明显:景区评论里常见的“门票”“索道”“民宿”这类领域词,通用词向量覆盖往往不好,自己训练可以拿到更贴合的语义空间。缺点是训练语料量不够时低频词向量会不太稳。所以我一般会在数据预处理时统计词频,把出现次数过低的词直接并到UNK里,而不是强行保留。这个决策直接影响后面的vocab_size和embedding矩阵尺寸,属于先定下来再往后走的参数。

2.2 Bi-LSTM编码层:双向信息聚合与参数边界

LSTM擅长处理序列依赖,但单向LSTM对后文信息是无感的。“虽然景色不错,但是设施陈旧”这类转折句,前面的“不错”会被后面“陈旧”修正,只看前文就很容易判错。Bi-LSTM的做法是把句子正着读一遍、倒着读一遍,每个时间步同时拿到前文和后文的信息,再拼接成该位置的隐状态。对短文本评论来说,这种双向编码比单向LSTM稳定不少。

项目名里的ABiLSTM_Att,核心就是Bidirectional LSTM加Attention。整体配置上,Embedding层输出200维向量,Bi-LSTM的hidden_units设在128左右,dropout取0.5。选这些参数时有个度:hidden_units太小,转折句的上下文关系装不下;太大,小数据集上又会过拟合。景区评论单条通常几十个字,序列长度截到64基本够用。过长句子的尾部信息注意力会自己分配权重,不用靠硬截,这一点后面会在注意力部分展开。

提示:不要一上来就把hidden_units拉高到256以上。先跑通小规模训练,确认loss能降下来,再逐步放大。

2.3 注意力机制:让模型知道“哪几个词决定了评价”

评论里不是每个词都对情感等价。“很美”“太差”“居然”这类词对预测的贡献远大于“我们”“然后”。注意力机制就是给每个时间步算一个权重分数,再按权重把Bi-LSTM的隐状态加权求和,让分类头主要依据情感词所在的隐状态做判断,而不是把整句平均掉。

常见做法是加性注意力或点积注意力。对128维的隐状态,先做线性变换映射到打分空间,softmax归一化成权重,再对隐状态做加权平均。实现上如果直接拿Bi-LSTM输出做self-attention,要注意mask的处理:PAD位置没有实际含义,打分时要被mask掉,否则填充的0也会参与权重计算,预测结果会漂。这也是TensorFlow里mask_zero=True存在的意义,模型在Embedding层就知道哪些位置是填充位。

2.4 对抗训练:对Embedding加扰动,给模型“打预防针”

“融合对抗”是这份源码区别于普通Bi-LSTM的关键点。对抗训练不是换网络结构,而是训练策略:对Embedding层的输入表示加一个微小扰动,让模型在“被干扰”的情况下仍然分得对。这样得到的模型对同义词替换、输入噪声更有韧性,在小样本评论数据上通常能把F1拉高几个点。

在文本分类里最常用的是FGM(Fast Gradient Method),按梯度方向给embedding叠加一个delta。delta太小没有扰动效果,太大反而破坏语义。epsilon我一般放在0.3到0.8之间试,先固定其他超参,单独调这一个值看验证集变化。下面这段就是核心的扰动计算片段:

# FGM 对 embedding 参数施加梯度扰动 import tensorflow as tf def fgm_perturb(model, inputs, labels, epsilon=0.5): # 定位 embedding 层:这里以 model.layers[1] 代替,实际要按模型结构确认 embed_layer = model.layers[1] with tf.GradientTape() as tape: logits = model(inputs, training=True) loss = tf.reduce_mean( tf.keras.losses.sparse_categorical_crossentropy(labels, logits) ) grads = tape.gradient(loss, embed_layer.trainable_variables) # 归一化梯度方向,乘以 epsilon 得到扰动 delta delta = [ epsilon * g / (tf.norm(g) + 1e-8) for g in grads if g is not None ] # 先记录原值,后续加扰动计算对抗 loss 后要恢复 backup = [tf.identity(v) for v in embed_layer.trainable_variables] for v, d in zip(embed_layer.trainable_variables, delta): v.assign_add(d) return backup, delta

这段代码只负责“加扰动”这一步。训练循环里的完整做法是:先用正常batch算一次loss并更新梯度,再对embedding加扰动、用同一个batch再算一次loss,然后恢复embedding原始参数,把两次梯度合并更新。delta的计算里加了一个1e-8,是为了防止梯度全零时除零报错。embedding层索引在Keras里不一定是固定数字,我用model.layers[1]只是示意,实际复现时先打印model.summary()确认哪个层是Embedding再改。

需要说明的是,对抗训练并不改变分类头的输出维度,也不影响换2class还是3class数据集,它只是让embedding空间的表示更稳。如果加上对抗后验证集反而震荡,优先把epsilon降一半,再看学习率是不是没跟着调低。

3. 工程结构与复现:config、dataSet、model、tools四个文件怎么协作

3.1 先看清目录里的角色分配

一个完整的实训工程,最怕的是代码和文件名对不上号。这个包里没有把全部逻辑塞进一个main脚本,而是按职责拆成配置、数据、模型、工具四个模块,外加三个notebook作为不同入口。我第一遍复现时,就是照着文件清单一个个确认角色的。

文件 / 目录角色关键输出
ABiLSTM_Att主代码目录收纳所有模块与notebook
config.py全局配置路径、超参数、类别数
dataSet.py数据预处理分词、去停用词、序列填充、word2id
tools.py工具函数序列长度计算、评价指标
model.py模型定义Bi-LSTM + Attention 网络
gensim_word2vec.ipynb词向量训练word2vec模型
main_save.ipynb训练入口保存训练好的模型
main_load.ipynb预测入口加载模型做情感预测
data/corpus标注数据3class、2class两份语料
stopwords停用词表过滤无意义词
model_save / model_load模型保存与加载目录训练产物与预测加载对象
logs / result训练产出日志、预测结果

这里有一个值得注意的点:data目录下同时放了3class和2class两份标注数据,说明项目从二分类扩展到了三分类。前者只分好评差评,后者加了中评或一般。切换分类数时,不只是改config里的NUM_CLASSES,还要确认语料标签文件和预处理后的标签编码对齐,否则训练和预测根本对不上。

3.2 config.py:所有改动的总开关

config.py是复现时最先要看、也最先要改的文件。路径写死会导致换机器就报FileNotFoundError,超参数写错则模型完全跑飞。我一般会先把所有路径改成相对项目根目录的形式,再逐项核对超参。

# config.py 配置文件(常见布局) import os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) # 数据路径:corpus 下按 3class / 2class 分目录存放 TRAIN_DATA = os.path.join(BASE_DIR, "data", "corpus", "3class", "train.txt") TEST_DATA = os.path.join(BASE_DIR, "data", "corpus", "3class", "test.txt") STOPWORDS_PATH = os.path.join(BASE_DIR, "stopwords", "stopwords.txt") # 产出路径:word2vec、模型、日志、结果各归各的 WORD2VEC_PATH = os.path.join(BASE_DIR, "word2vec", "wv.model") MODEL_SAVE_DIR = os.path.join(BASE_DIR, "model_save") LOG_DIR = os.path.join(BASE_DIR, "logs") RESULT_DIR = os.path.join(BASE_DIR, "result") # 模型超参数 EMBEDDING_DIM = 200 # 词向量维度 HIDDEN_UNITS = 128 # Bi-LSTM 隐藏单元数 NUM_CLASSES = 3 # 3class:0/1/2,换 2class 时改成 2 MAX_LEN = 64 # 单条评论统一截断填充长度 BATCH_SIZE = 64 EPOCHS = 30 LEARNING_RATE = 0.001 DROPOUT = 0.5

参数说明放在这里。EMBEDDING_DIM要和word2vec训练时的vector_size保持一致,不一致的话加载词向量后维度直接报错。NUM_CLASSES是这里最容易翻车的字段。MAX_LEN不要拍脑袋定,应该先跑tools.py里的compute_max_len统计语料长度分布,再取一个分位数。EPOCHS设30不是让你一定跑满,配合早停回调,验证集连续几轮不升就停。

提示:换2class数据集时,先改NUM_CLASSES,再跑一次compute_max_len,别让MAX_LEN沿用旧值。

3.3 dataSet.py:把中文评论变成序号序列

dataSet.py是数据流水线的核心。它要干四件事:分词、去停用词、构建词表、把词序列转成定长序号序列。这里的处理质量直接决定后面训练能不能收敛。很多“模型训练不上去”的求助,最后都发现是数据侧出了问题,比如全角半角混用、停用词表没加载、标签文本混在评论里没拆开。

# dataSet.py 数据预处理模块(核心逻辑示意) import jieba from collections import Counter from tensorflow.keras.preprocessing.sequence import pad_sequences def load_stopwords(path): with open(path, "r", encoding="utf-8") as f: return {line.strip() for line in f if line.strip()} def tokenize(text, stopwords): # jieba 分词,去掉停用词和空白字符 return [w for w in jieba.lcut(text.strip()) if w.strip() and w not in stopwords] def build_vocab(tokenized_texts, min_count=1): counter = Counter() for words in tokenized_texts: counter.update(words) # 按词频过滤低频词,过滤后的词统一走 UNK word2id = {w: i + 1 for i, (w, c) in enumerate(counter.items()) if c >= min_count} word2id["<UNK>"] = len(word2id) + 1 return word2id def text_to_sequence(tokenized_texts, word2id, max_len): seqs = [[word2id.get(w, word2id["<UNK>"]) for w in words] for words in tokenized_texts] # 统一到 max_len,后面填充,前面保留 return pad_sequences(seqs, maxlen=max_len, padding="post", truncating="post")

这段逻辑里有两个细节容易看走眼。一是word2id从1开始编号,把0留给了PAD,这样模型在Embedding层设置mask_zero=True后,填充位置就不会参与后续注意力计算。二是build_vocab时用min_count过滤低频词,低频词统一映射到 ,这样测试集里出现没见过的词也不会崩溃。min_count不建议设太大,景区评论语料本身不大,设1到2比较稳。

3.4 model.py与tools.py:模型定义和评估工具

model.py里的模型是baseline,也就是整个工程的主网络。它从Embedding层进去,经过双向LSTM拿到每个位置的隐状态序列,再用注意力机制把序列加权成一个句子向量,最后接Dropout和softmax分类。这个结构不复杂,但每一步的shape都要对得上,尤其是Bi-LSTM的return_sequences必须设为True,否则注意力层拿到的是最后一个时间步的向量,没法做时间步加权。

tools.py承担序列长度计算和评价指标计算。compute_max_len用于生成MAX_LEN配置,compute_metrics则输出accuracy、precision、recall、F1,这些指标比只看accuracy更能反映类别不平衡时的表现。比如某景区“好评”占了百分之七八十,一个全预测成好评的模型accuracy也好看,但F1会直接暴露问题。

# tools.py 工具函数(常见实现) import numpy as np from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score def compute_max_len(tokenized_texts, quantile=0.95): # 按序列长度分位数确定 MAX_LEN,避免被超长文本拉高 lengths = [len(t) for t in tokenized_texts] return int(np.quantile(lengths, quantile)) def compute_metrics(y_true, y_pred): return { "accuracy": accuracy_score(y_true, y_pred), "precision": precision_score(y_true, y_pred, average="macro"), "recall": recall_score(y_true, y_pred, average="macro"), "f1": f1_score(y_true, y_pred, average="macro"), }

复现时注意compute_max_len的输入是分词后的token列表,不是原始字符串,直接用原文会得到偏大很多的长度分布。另外precision这些指标在换到3class时仍然适用,但average方式要看数据分布,类别特别不均时用macro比weighted更严格,能看出模型在小类上的真实表现。

4. 训练与预测全流程:从gensim词向量到main_save与main_load两个入口

4.1 gensim_word2vec.ipynb:先训练一份自己的词向量

整个流程的起点是词向量。运行gensim_word2vec.ipynb时,会用当前语料的所有分词结果训练Word2Vec,得到每个词的向量表示。注意Gensim 4.1.2版本的API和3.x时代差别很大,model.wv才是词向量本体,旧教程里model[word]这种写法现在会报KeyError。

# gensim_word2vec.ipynb 核心片段 import jieba from gensim.models import Word2Vec def load_train_texts(path): texts = [] with open(path, "r", encoding="utf-8") as f: for line in f: # 标签与评论文本用 \t 分隔,只取文本部分 parts = line.strip().split("\t") if len(parts) >= 2: texts.append(parts[1]) return texts raw_texts = load_train_texts("data/corpus/3class/train.txt") stopwords = {w.strip() for w in open("stopwords/stopwords.txt", encoding="utf-8")} sentences = [jieba.lcut(t) for t in raw_texts if t.strip()] # sg=1 用 skip-gram;景区评论语料小,vector_size 设 200 即可 w2v_model = Word2Vec(sentences, vector_size=200, window=5, min_count=1, sg=1, epochs=10) w2v_model.save("word2vec/wv.model")

这段代码里window控制共现窗口大小,窗口越大越偏主题信息,越小越偏词法信息,评论类短文本我一般用5。sg=1即skip-gram,对低频词更友好。训练后不要忘了执行w2v_model.wv.vectors.shape确认词表大小和向量维度,这个shape要和config.py里的vocab_size、EMBEDDING_DIM对齐。

这里有个常见误区:word2vec训练和主模型训练是两回事。词向量一旦训练完成,后面主模型是把每个词替换成对应的向量序号再查表,而不是在训练主模型时单独跑word2vec。所以语料中有新词时,要回到这一步重新训练词向量,或者把新词映射到 ,不能直接往词表里硬塞。

4.2 main_save.ipynb:训练主流程与模型保存

main_save是训练入口。它的职责是读取训练数据、执行预处理、构建模型、跑batch训练、保存模型和日志。项目里这个文件叫save,意思是训练结束后把权重落盘,供另一个入口load去预测。

# main_save.ipynb 训练主流程(示意,按实际调用简写) import numpy as np import tensorflow as tf from config import * from dataSet import load_stopwords, tokenize, build_vocab, text_to_sequence from model import build_model from tools import compute_max_len texts, labels = read_dataset(TRAIN_DATA) # 按实际数据格式解析 stopwords = load_stopwords(STOPWORDS_PATH) tokenized = [tokenize(t, stopwords) for t in texts] max_len = compute_max_len(tokenized, quantile=0.95) # 替代手写 MAX_LEN word2id = build_vocab(tokenized, min_count=1) X = text_to_sequence(tokenized, word2id, max_len) y = np.asarray(labels) model = build_model(len(word2id) + 1, EMBEDDING_DIM, max_len, HIDDEN_UNITS, NUM_CLASSES, DROPOUT) model.compile(optimizer=tf.keras.optimizers.Adam(LEARNING_RATE), loss="sparse_categorical_crossentropy", metrics=["accuracy"]) model.fit(X, y, batch_size=BATCH_SIZE, epochs=EPOCHS, validation_split=0.1) model.save(MODEL_SAVE_DIR)

这里的read_dataset是示意函数,实际在dataSet.py里要从train.txt按分隔符解析出标签和文本,注意标签要做成0、1、2这样的整数,对应分类头的输出顺序。损失函数用sparse_categorical_crossentropy意味着标签不转one-hot,直接传整数就行。validation_split=0.1表示训练集里自动切10%做验证,如果数据集已经分好train/test,建议改成从train里手动均匀抽样验证集,避免分布倾斜。

模型保存用model.save(MODEL_SAVE_DIR)保存完整模型,包括结构、权重和优化器状态。下次加载时用tf.keras.models.load_model(MODEL_SAVE_DIR)就可以直接预测。要注意保存路径下不能混入旧版本模型文件,否则load容易加载到结构不一致的h5或SavedModel,报错信息还不太直观。

4.3 main_load.ipynb:加载模型对新评论做情感预测

训练完成后,main_load承担预测任务。它的输入是未标注语句,输出是情感类别和概率。这里的处理必须和训练时完全一致:分词方式、停用词表、word2id构建、序列填充顺序,任何一个不对,预测结果都是乱的。特别是word2id,训练时是临时构建的,加载预测时也要按同样规则重建,不能拿训练时的词表对象硬套。

# main_load.ipynb 预测入口核心逻辑 import numpy as np import tensorflow as tf from config import * from dataSet import load_stopwords, tokenize, build_vocab, text_to_sequence model = tf.keras.models.load_model(MODEL_SAVE_DIR) def predict_single(text, word2id, max_len): stopwords = load_stopwords(STOPWORDS_PATH) words = tokenize(text, stopwords) seq = text_to_sequence([words], word2id, max_len) prob = model.predict(seq, verbose=0)[0] label = int(np.argmax(prob)) return label, prob

预测函数返回的label是类别索引,prob是各类别概率数组。要输出给人看的“好评/中评/差评”文字,还需要在函数外面做一次索引映射,比如{0: "差评", 1: "中评", 2: "好评"}。

注意:word2id必须在训练和预测两侧保持一致,最好在训练后保存成json,预测时直接加载,而不是临时重建。之前遇到过一次很隐蔽的问题:训练时word2id的构建顺序基于词频Counter,同样的语料每次运行顺序是稳定的,但换机器或换Python版本后Counter顺序可能有差异,生成的word2id不一致,导致训练和预测词表错位。

5. 复现必读的五个坑:数据、维度、环境与结果排查记录

5.1 Gensim 4.x的API差异:model[word]报KeyError

现象:按老教程写的w2v_model["风景"]取词向量,结果抛出KeyError,换成w2v_model.wv["风景"]就正常。

原因:Gensim 4.1.2调整了接口,词向量统一收在model.wv里,旧版可直接索引的方式被移除。

解决:把取向量、查相似词等操作全部切到w2v_model.wv上,比如w2v_model.wv.vectors、w2v_model.wv.most_similar("风景")。保存推荐用model.save("wv.model"),加载用Word2Vec.load,不用word2vec_format的二进制,否则还要额外传词表文件,多一道出错机会。

5.2 TensorFlow 2.3与高版本Numpy的兼容冲突

现象:加载模型或执行predict时报numpy相关错误,比如维度无法相乘或属性不存在。

原因:TensorFlow 2.3.0对numpy 1.24及以上版本的兼容有限制,环境里numpy版本偏高就会在运算时踩到不兼容。

解决:按项目列出的环境配置建独立conda环境,锁Python 3.8.12,再安装tensorflow==2.3.0和numpy==1.19.5。如果已经有高版本环境,不要直接降级当前环境,新建一个环境花五分钟,比排查半天numpy报错省时间。

5.3 embedding维度或vocab_size对不上,模型尺寸报错

现象:模型训练第一轮就报维度不匹配,比如Incompatible shapes,或者词查找时找不到某个key。

原因:word2vec训练的vector_size是200,但config里EMBEDDING_DIM被改成100;或者vocab_size少算了UNK这一项。

解决:打印三个值核对——w2v_model.wv.vectors.shape、len(word2id)、config里的EMBEDDING_DIM。vector_size和EMBEDDING_DIM必须一致;Embedding层的input_dim要填vocab_size,也就是len(word2id)加1,因为索引从1开始且UNK占用一个位置,0留给了PAD。打印确认后再改config,不要凭感觉猜。

5.4 2class和3class切换后,标签维度对不上

现象:数据换成2class目录后,训练时loss正常下降,但预测时概率数组长度还是3,或者准确率忽高忽低。

原因:只改了数据路径没改NUM_CLASSES,分类头还是3个输出,2class的标签0和1对应的语义在输出层错位了。

解决:换数据目录时同步改config里的NUM_CLASSES,并且确认dataSet.py里标签编码时用的映射和输出层顺序一致。我习惯在main_save开头打印model.output.shape和y里的类别集合,一眼就能看出是否对齐。

5.5 直接用训练集评估,F1虚高

现象:日志里F1接近0.95,但换一批新评论预测结果却惨不忍睹。

原因:不少notebook为了演示方便直接在训练集上算指标,模型已经记住了训练数据,评估结果自然虚高。

解决:从train.txt里单独抽15%到20%作为验证集,训练时只用剩下部分,或者用validation_split=0.15,然后用tools.py的compute_metrics在验证集上算指标。如果用的是分好的2class/3class目录,确保test数据没有混进训练过程。

6. 进阶用法:用预测概率生成景区评价分数并校验置信度

6.1 把情感概率映射成1到5分

分类输出的概率本身信息量比标签大。同样是好评,概率0.99和0.51的置信度完全不同。我通常会在这里加一个转换层,把3类概率加权映射成景区常用的五分制评价分数:好评给高分权重,差评给低分权重,中评按中性处理。这样批量评论分析后,可以直接得到一个景区的综合满意度分。

# 示例:把预测概率映射为五分制评价分 def prob_to_score(prob, label_map={0: 1.0, 1: 3.0, 2: 5.0}): return float(np.dot(prob, [label_map[i] for i in range(len(prob))])) # 使用示例 label, prob = predict_single("缆车排队两小时,风景确实不错,但体验太差", word2id, max_len) score = prob_to_score(prob) if max(prob) < 0.6: score = None # 置信度过低,交给人工复核

映射权重的设计要结合业务:差评权值给1分、中评3分、好评5分,模型输出概率后做加权平均。放在真实景区评论场景里,我不建议直接输出小数分,而是按0.5分一档取整,这样对外展示时更符合习惯。

6.2 置信度阈值与批量落盘

置信度小于0.6的样本先标记成待人工复核,而不是硬给一个分数,这是上线前很重要的护栏逻辑。项目里的result目录就是用来放这些预测结果和评分输出的。批量场景下,我会把每条评论的原始文本、预测标签、三项概率、映射分数写进同一行,便于回溯哪条样本被阈值拦下来了。

我从这套源码包里拿到的核心收获是:对抗训练和注意力不是玄学,而是有明确收益的工程选择。需要复现这份资源时,记得先把data目录和config对齐再开跑,按第5章的排查顺序走完一遍,能省不少时间。从那以后我每次处理这类带对抗训练的文本分类工程,都会先花十分钟检查config、两个notebook的入口路径和类别配置,再决定要不要动网络结构。这个习惯帮我少翻了很多次车,希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进