简介:这是一个基于预训练模型BERT、BERT-wwm的新闻情感分析系统Python源码包,面向计算机相关专业学生作为课程设计、毕业设计或期末大作业,也适合入门自然语言处理文本分类的开发者。包内含完整项目代码、说明文档与训练数据集,共128个文件,以Python脚本(70个py)、预编译pyc、shell训练脚本、配置说明txt、数据csv及说明md为主,整体约2.72MB。项目实现了从数据kfold处理、数据信息查看、预训练模型文本分类(支持roberta_wwm_ext_large、roberta_large等)到模型集成投票、自动存档的完整流程,目录结构清晰,包含backup-models、data、pretrained_model、ensemble_submits等模块。已有255人浏览学习,适合希望快速上手BERT系列模型做情感分析、需要完整可运行基线方案的读者,基于此二次开发或扩展为毕设演示均可。
1. 基于预训练模型BERT、BERT-wwm的新闻情感分析系统:这套源码到底值不值得跑
新闻情感分析一直是NLP落地里最“攒劲”的场景:热点事件出来,运营想立刻知道舆论是正、是负还是中性;媒体小编想判断稿子倾向;舆情公司更是不停地给新闻正文打情感标签。以前大家用情感词典,准确率不稳,换个领域就崩。现在预训练模型BERT和它的升级变体BERT-wwm(全词掩码)成了标配,用项目源码加数据集就能本地训练出可用模型。这篇笔记围绕“python源码+项目说明+数据集”这条线,说的是复现这套系统该做什么,以及最值得花时间的几件事。这类项目真正值钱的不是那几行模型调用的代码,而是数据清洗、标签映射、训练参数和模型文件管理这些地面功夫。
2. 把新闻数据集喂进BERT前:清洗、标签检查和文本长度分布
很多人拿到源码包第一步就急着跑train.py,结果要么报错,要么模型loss一直不降。我一般不会直接动训练脚本,而是先花一小时把数据集摸一遍。数据决定上限,BERT只是帮你逼近这个上限。源码附带的新闻数据集不管是什么格式,先明确三件事:字段含义、标签分布、文本长度。
2.1 大多数新闻情感数据集长什么样:从类别字段到标签映射
这类数据集最常见的结构是TSV或CSV两列,一列是新闻标题或正文,一列是情感标签。标签有分正/负二分类的,也有带中性的三分类,还有极小众的五分类(非常负面、负面、中性、正面、非常正面)。做技术复现时第一件事不是写模型,而是先把标签字段稳定成整数索引。
我拿到CSV后会先做一次结构体检:
import pandas as pd from collections import Counter df = pd.read_csv("news_dataset.csv", sep="\t") # 很多新闻数据集用\t分隔 print(df.columns.tolist()) print(df.head(3).to_string()) print("shape:", df.shape) # 检查标签取值和数量分布,这一步决定loss函数怎么写 label_counter = Counter(df["label"]) print(label_counter)逻辑说明:sep="\t"是因为很多新闻语料是爬虫导出,用制表符分隔比逗号更不易出错——新闻正文里全角逗号、半角逗号到处都是,用csv读反而容易裂列。Counter统计每个类别的样本数,目的是判断目标是否均衡。如果三类样本比例像7000:6800:6500这种,属于可训练范围;如果出现10000:2000:500这种,就要准备做类别加权或欠采样。
参数说明:当标签是三分类时,模型输出维度设3;二分类设2。注意有些数据集里的情感标签写的是中文,比如“正向”“负向”“中性”,必须要做映射:
label_map = {"正向": 0, "负面": 1, "中性": 2} # 如果不想要中性维度,可以合并成二分类新闻情感分析很特殊的一点是:负面新闻天生容易识别,中性文本最难分。因为新闻讲究客观陈述,大量政治、财经稿子情感词极少,模型很容易把“中性”学成“负面”。这就是后面所有训练策略都要围绕标签分布展开的原因。
2.2 BERT自带Tokenizer做子词切分:为什么不需要先跑jieba
传统文本分类的第一道工序是分词。但使用BERT家族时,这一步完全被BertTokenizer替代。BERT用的是WordPiece子词切分,中文场景下它的bert-base-chinese权重是整字切分——每个汉字是一个token,英文和数字会被切成子词。所以新闻标题里“苹果公司发布iPhone 15”会切成“苹 果 公 司 发 布 i phone 15”这种token序列。
我见过有人非要先用jieba.cut()把新闻分词,再把词用空格串起来喂给BERT,这种做法在中文BERT上属于画蛇添足。因为预训练权重就是按不带分词的文本训练的,强行分词等于改变输入分布。真正需要做的事情是清洗脏字符:
from transformers import BertTokenizer tokenizer = BertTokenizer.from_pretrained("bert-base-chinese") def clean_text(text: str) -> str: # 新闻爬虫文本经常带网页噪声,去掉多余空白和特殊符号 text = text.replace("\\u3000", "").replace("\u3000", " ") text = text.replace("\\xa0", " ") # 不断行空格 text = " ".join(text.split()) # 统一连续空白 return text.strip() # 验证tokenizer在正常长句上的输出 sample = "专家表示此次降价对市场影响有限,消费者仍需理性看待。" tokens = tokenizer.tokenize(clean_text(sample)) print(tokens[:20]) print("token数:", len(tokens))逻辑说明:clean_text解决的是CSV解析后残留的全角空格和\xa0,新闻网页文本常出现这类隐藏字符,不过滤会污染token序列。tokenizer.tokenize()返回的是普通字符串列表,便于肉眼检查切分是否正常,正式训练用encode_plus或__call__得到input_ids。
参数说明:这里用的bert-base-chinese是中文BERT预训练权重,HuggingFace内置下载。它的词表有21128个token,足以覆盖简体新闻文本。如果数据集包含大量粤语、英文俚语、表情符号,可以考虑换bert-base-multilingual-cased,但中文新闻场景下中文权重普遍更好。
2.3 构造PyTorch Dataset并统计类别权重:先解决类不均衡再谈训练
数据预处理最终要变成PyTorch能吃的Dataset结构。一个踩过坑的地方是:很多源码里的Dataset返回的是(batch, label),但没有把attention_mask处理好。新闻句子长短不一,padding得多的话,mask必须同步生成,否则模型会把padding当成真实文本,严重影响收敛。
我一般会这样构造:
import torch from torch.utils.data import Dataset class NewsDataset(Dataset): def __init__(self, texts, labels, tokenizer, max_len=128): self.texts = texts self.labels = labels self.tokenizer = tokenizer self.max_len = max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): text = clean_text(self.texts[idx]) encoding = self.tokenizer.encode_plus( text, max_length=self.max_len, padding="max_length", truncation="longest_first", return_tensors="pt" ) # 把encode_plus返回的张量去掉batch维度,否则dataloader会变成三维 item = { "input_ids": encoding["input_ids"].squeeze(0), "attention_mask": encoding["attention_mask"].squeeze(0), "labels": torch.tensor(self.labels[idx], dtype=torch.long) } return item逻辑说明:truncation="longest_first"是最适合新闻文本的截断策略——从最长的一侧开始删token,保住句子主干,而不是硬切前128个字。padding="max_length"保证了batch内部形状一致,虽然会浪费一些算力,但换来的是训练循环里不需要写任何动态padding逻辑,新手复现不容易翻车。
参数说明:max_len=128对新闻标题足够,新闻正文一般也建议控制在256以内。不是越长越好,BERT对512token的注意力矩阵计算复杂度是平方级,长度拉长一倍,显存占用可能变成四倍。数据体量大时可以先统计所有新闻的token长度,取P90作为max_len:
lengths = [] for text in df["text"].tolist()[:20000]: lengths.append(len(tokenizer.encode(text, truncation=False))) p90 = sorted(lengths)[int(len(lengths) * 0.9)] print("P90 token len:", p90)这步能快速判断数据集的文本长度形态。很多新闻正文的P90只有200左右,这时max_len设256很划算。
类别不均衡则在训练时用weighted sampler或CrossEntropyLoss(weight=...)来解决。二分类正负比4:1时,固定学习率训练很容易把多数类准确率刷到95%,少数类只有40%。我喜欢先把权重传进loss:
from torch.nn import CrossEntropyLoss class_weights = torch.tensor([1.0, 2.5, 1.8]) # 根据2.1统计结果调整 criterion = CrossEntropyLoss(weight=class_weights)权重设多少不是玄学,直接取负样本占比的倒数再归一化。比如负样本是正样本的2.5倍,就把负类的权重设为1.0,正类设为2.5。
3. 在BERT和BERT-wwm之间选型:微调流程与关键差别
数据集准备好了,接下来是模型选型。BERT和BERT-wwm尤其适合新闻情感分析:中文新闻里很多高频词(“不排除”、“有望”、“风险”)是两三个字的词组,原版BERT训练时用的是随机单字掩码,会把词拆开破坏语义;BERT-wwm的Whole Word Masking做法是每次遮住整个中文词,模型被迫从更完整的上下文学习词边界。新闻这种讲究庄重语体的文本,全词信息往往比单字更利于情感判断。
但我必须诚实说,BERT-wwm在新闻情感任务上跟BERT的差距经常只有0.5%-1.5%F1,并不像论文里那样神话。花时间调max_len和类权重,收益可能更大。
3.1 加载预训练模型与Tokenizer:指定local_files_only防止联网下载中断
源码里最常见的是用HuggingFace的BertForSequenceClassification直接加载。这个类自动帮我们把BERT的输出接到分类头上。很多新手在第一次加载时碰到网络问题,下载一半中断,然后整个环境卡住。我的做法是先用命令行把权重下到本地缓存,再在脚本里指定local_files_only进行离线加载。
from transformers import BertForSequenceClassification, BertTokenizer model_name = "hfl/chinese-bert-wwm-ext" # BERT-wwm权重,中文全词掩码预训练 # 第一次运行时联网下载,之后权重会落在本地缓存目录 tokenizer = BertTokenizer.from_pretrained(model_name) # 加载模型时可以关掉远程检查,避免因为网络抖动反复下载 model = BertForSequenceClassification.from_pretrained( model_name, num_labels=3, local_files_only=False # 设True后只读缓存,没有权重会直接报错 )逻辑说明:hfl/chinese-bert-wwm-ext是常用的中文BERT-wwm扩展模型。local_files_only=False时,HuggingFace会先查缓存,没有才走联网;设成True则强制离线,适合服务器内网部署时使用。训练阶段保持False没关系,推理部署阶段建议在本地先把模型目录复制出来,再开local_files_only=True。
参数说明:num_labels=3对应前面的三分类标签。不要手动去改BertForSequenceClassification里的classifier层维度之外的东西,预训练权重加载时会自动初始化随机参数,直接改模型结构容易导致维度不匹配报错。
3.2 分类头设计:CLS输出、pooled输出和平均池化该选谁
BertForSequenceClassification内部的分类头默认取[CLS]token的最终隐层输出去做线性分类。但实际项目中我经常把pooler_output也拎出来看一眼。HuggingFace的BertModel返回四个东西:last_hidden_state、pooler_output、hidden_states、attentions。pooler_output是BERT内部对[CLS]再过一层全连接和tanh后的结果,新闻情感分类这种短句场景两者差异不大,长文本时pooler_output往往更稳。
如果你想自己搭分类头,常见做法是:
import torch.nn as nn class NewsSentimentHead(nn.Module): def __init__(self, bert_model, hidden_size=768, num_labels=3): super().__init__() self.bert = bert_model self.dropout = nn.Dropout(0.3) # 新闻文本噪声大,dropout比默认0.1要激进 self.fc = nn.Linear(hidden_size, hidden_size) self.out = nn.Linear(hidden_size, num_labels) self.activation = nn.GELU() def forward(self, input_ids, attention_mask): outputs = self.bert(input_ids, attention_mask=attention_mask) pooled = outputs.pooler_output # [batch_size, hidden] pooled = self.dropout(pooled) pooled = self.fc(pooled) pooled = self.activation(pooled) pooled = self.dropout(pooled) return self.out(pooled)逻辑说明:加一层hidden_size的全连接主要让模型有更强的非线性拟合能力,但新闻情感分类不需要复复杂大网络,一层就够。dropout提高到0.3是因为新闻文本里低频词、人名机构名很多,模型容易快速记住关键词,导致验证集掉点。
参数说明:hidden_size=768对应BERT-base隐藏层维度。命题系统若用BERT-large,这里应改成1024。源码里如果写死了768,换成large权重会报维度错误。
3.3 微调训练循环:学习率、batch_size、warmup和梯度裁剪
训练BERT的细节比模型结构更影响结果。新闻情感分析用BERT微调,我踩过最大的坑是把学习率照搬其他CNN项目的1e-3。BERT是预训练参数,学习率过大会直接把原有词向量冲乱。稳定区间通常是1e-5到5e-5。下面是一段完整的训练核心循环。
from transformers import AdamW, get_linear_schedule_with_warmup learning_rate = 2e-5 batch_size = 16 epochs = 4 warmup_steps = int(0.1 * len(train_dataloader) * epochs) optimizer = AdamW(model.parameters(), lr=learning_rate, weight_decay=0.01) scheduler = get_linear_schedule_with_warmup( optimizer, num_warmup_steps=warmup_steps, num_training_steps=len(train_dataloader) * epochs ) model.to(device) for epoch in range(epochs): model.train() total_loss = 0.0 for step, batch in enumerate(train_dataloader): batch = {k: v.to(device) for k, v in batch.items()} outputs = model(**batch) loss = outputs.loss # BertForSequenceClassification自带的loss loss.backward() # 新闻长文本容易产生梯度爆炸,必须裁剪 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() scheduler.step() optimizer.zero_grad() total_loss += loss.item() avg_loss = total_loss / len(train_dataloader) print(f"epoch {epoch} avg_loss {avg_loss:.4f}")逻辑说明:outputs.loss是BertForSequenceClassification内部根据num_labels自动计算的交叉熵,省去手写criterion。clip_grad_norm_设max_norm=1.0是关键,长新闻样本会出现个别极端token导致梯度冲高,不裁剪loss曲线会突然跳到NaN。scheduler.step()放在optimizer.step()之后,这是PyTorch里线性学习率调度器的标准顺序。
参数说明:batch_size=16在单卡12GB显存下基本是上限;如果用CPU训练,batch_size降到8或4,且max_len要相应调小。warmup_steps设为总训练步数的10%,能够让模型在前期平稳起步,不产生大震荡。
3.4 BERT-wwm对比实验:同数据、同参数、同随机种子才可信
项目说明里既然同时给出BERT和BERT-wwm,必然要跑对比实验。最容易犯的错是只换model_name,但数据集划分乱了。对比实验必须固定随机种子,否则类别不均衡的数据集每次划分出来的训练验证集分布都不同,结论毫无意义。
我把靠谱的对比做法写清楚:
import random import numpy as np import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) set_seed(42) # 数据集划分也必须set_seed后再操作在相同训练代码里,只改一行:
model_name = "bert-base-chinese" # 实验组A model_name = "hfl/chinese-bert-wwm-ext" # 实验组B记录对比时不要只看准确率,要看每个类别的F1。新闻情感三分类里,中性类F1通常最低,BERT-wwm的优势恰恰体现在中性类能拉高1-2个点。如果那个数据集中性类占比不到10%,对比意义有限,这个结论在项目说明里值得写清楚。
4. 把训练好的模型变成可用的新闻情感分析系统:源码里的最小完整闭环
模型训练只是第一步,源码包里的“系统”还要能对新的新闻做预测。很多人训练完在Notebook里能出结果,但没法集成到业务里,往往因为模型文件管理混乱。这一章给出一条最简单的闭环路径。
4.1 模型文件家族:config.json、pytorch_model.bin、vocab.txt不能拆散
微调完成后,模型要保存成HuggingFace的标准文件格式。一个常见的翻车是只保存pytorch_model.bin,下次加载时报各种维度错。标准保存方法如下:
save_dir = "./news_sentiment_model" model.save_pretrained(save_dir) tokenizer.save_pretrained(save_dir)save_pretrained会同时写出三类关键文件:
news_sentiment_model/ ├── config.json # 模型结构配置,记录num_labels=3 ├── pytorch_model.bin # 模型权重 ├── vocab.txt # tokenizer词表 └── tokenizer_config.json以后重新加载时只需用同一目录路径,HuggingFace会从config.json推断分类头维度,不需要再手动传num_labels。配置文件、词表和权重文件三者必须放在同一个目录,只拷bin文件是复现系统时最常见的“黑匣子”问题。
4.2 封装predict函数:tokenizer、模型、标签映射三件套
预测函数的正确姿势是让输入永远是原始新闻文本,输出中文标签,而不是输出索引就完事。下面是带置信度的预测封装。
def predict_news(model, tokenizer, text, id2label, device, max_len=128): model.eval() encoding = tokenizer.encode_plus( clean_text(text), max_length=max_len, padding="max_length", truncation="longest_first", return_tensors="pt" ) input_ids = encoding["input_ids"].to(device) attention_mask = encoding["attention_mask"].to(device) with torch.no_grad(): logits = model(input_ids=input_ids, attention_mask=attention_mask).logits probs = torch.softmax(logits, dim=-1) pred_id = torch.argmax(probs, dim=-1).item() confidence = probs[0][pred_id].item() return id2label[pred_id], confidence逻辑说明:id2label是预测时最容易被搞错的一环。比如类别索引{ "负面": 0, "正面": 1, "中性": 2 }训练时和预测时用同一个映射字典就不会错位。常见的血泪经验是训练脚本里用LabelEncoder,顺手拿到索引,但没保存映射关系,预测时重新按字母排序,于是“正面”和“负面”互换,整个系统看起来完全失灵。正确做法是在训练结束后把映射表存成JSON。
import json label_map = {"负面": 0, "正面": 1, "中性": 2} id2label = {v: k for k, v in label_map.items()} with open("./news_sentiment_model/label_map.json", "w", encoding="utf-8") as f: json.dump(id2label, f, ensure_ascii=False, indent=2)4.3 批量预测与结果导出:CPU推理太慢时的兜底方案
新闻系统通常要批量处理上千条新闻。单条循环predict太慢,常见做法是让dataloader一次跑一批,再统一写回DataFrame。
def predict_batch(model, tokenizer, texts, id2label, device, batch_size=32, max_len=128): model.eval() results = [] for i in range(0, len(texts), batch_size): sub_texts = texts[i:i + batch_size] encoding = tokenizer( [clean_text(t) for t in sub_texts], padding=True, # batch内按最长样本padding,省算力 truncation="longest_first", max_length=max_len, return_tensors="pt" ) input_ids = encoding["input_ids"].to(device) attention_mask = encoding["attention_mask"].to(device) with torch.no_grad(): probs = torch.softmax(model(input_ids=input_ids, attention_mask=attention_mask).logits, dim=-1) preds = torch.argmax(probs, dim=-1).cpu().tolist() confs = probs.max(dim=-1).values.cpu().tolist() for idx, j in enumerate(preds): results.append((id2label[j], confs[idx])) return results这里padding=True不使用统一max_len,而是按batch内最长样本补padding,比固定max_len少算不少无效token。如果CPU推理实在慢,还有一个更接地气的方案:训练时如果没有GPU,就用蒸馏过的distilbert-base-chinese或albert-base-chinese,但那些不是本系统标题里的预训练模型。从项目落地角度,我建议数据量不超过5万条时直接GPU跑一次,然后离线缓存预测结果,别写成每来一条新闻实时调模型。
5. 避坑排查:本地复现这个系统的五个高频翻车点
这个部分直接给结论,每个坑都是我在类似源码包上实际踩过的。复现失败的原因往往不在模型,而在环境、数据和代码之间的默契。
5.1 现象:RuntimeError: CUDA out of memory,训练到第三个epoch崩溃
原因很多,最常见的是max_len=512加batch_size=16,显存被打爆。还有一个隐藏原因是PyTorch默认缓存机制导致,虽然batch_size小,但上一次batch的显存没释放,多次累加后OOM。
解决:先把batch_size调到8试一次,同时把max_len从512降到256。缩小模型不会掉太多效果,长文本显存占用是二次方的,降一半长度能省将近四倍量。如果还想保住大batch,改用梯度累积:每4步更新一次优化器,等于batch_size不变但实际显存占用只算一个小batch。
5.2 现象:loss降不下去,准确率稳定在50%附近,像是随机猜
原因:标签映射错位,尤其是News数据集的label字段包含了-1、0、1这种非从0开始的索引,而分类头期望的是0、1、2。还有一个常见原因是数据处理时把字符串标签直接传给了模型,训练数据里混入无法转float的值,PyTorch在dataloader阶段静默出错但未必崩溃。
解决:在预处理第一步打印label的unique()值,建立新映射统一到0起始。然后单独跑一个batch的前向验证输出维度。如果还不行,用tokenizer.decode(input_ids[0])人工看一眼训练样本是不是乱码。
5.3 现象:训练集loss一直在降,验证集F1忽高忽低,每一步都不稳定
原因:验证集太小,或者新闻数据集按时间排序后你直接切了前80%训练、后20%验证,导致训练和验证分布完全偏移。新闻文本的时间性很强,某段时间内高频出现同一类话题,模型学的是时间段特征不是情感特征。
解决:用sklearn.model_selection.train_test_split(stratify=y)按标签分层划分,不要按行号硬切。最好用五折交叉验证看稳定区间。验证集至少要有1000条,低于500条的验证曲线没有参考价值。
5.4 现象:把bert-base-chinese换成hfl/chinese-bert-wwm-ext后,预测结果和BERT完全一样
原因:模型加载后没有重新训练,或者训练代码里在对比实验时没把model_name传进from_pretrained,训练脚本里的模型名写死成了bert-base-chinese。还有一种情况是,训练过程没生效,前向直接用了预训练权重。
解决:换权重后先跑一步train,检查loss和训练前是否明显不同。如果loss没变,查看model.config里是否有model_type标记和_name_or_path,确认加载的确实是新权重。这种做法虽然土,但能杜绝“换了没生效”的翻车。保存模型时也可以在config.json里加一行自定义字段model_mark,防止下次混淆。
5.5 现象:HuggingFace下载权重时进度条卡死,卡在Downloading a file
原因:国内网络访问HuggingFace服务器不稳,或者没有开代理,但这里不展开网络设置。最常见的是下载到一半连接断开,HuggingFace缓存了不完整文件,之后每次加载都报md5 mismatch。
解决:把下载超时适当调大,用环境变量控制,或者直接在命令行下载到本地目录后拷贝到自己的模型目录。建议第一次加载时把local_files_only=False,确保权重完整落进缓存;后续用local_files_only=True离线加载。如果缓存损坏,可以删除本地缓存目录里对应模型名,重新下载一次。
6. 用五折交叉验证和置信度过滤,把源码系统调成自己能上手的版本
这是整套系统的最后一公里。新闻情感分析项目里,单次train/test的准确率有太多偶然性。我用过一次从源码包里直接跑出来的“92%准确率”模型,换了真实新闻数据不到60%。原因是验证集和测试集来自同一个月的话题,模型过拟合到话题词上。后来我用五折交叉验证重新选epoch,顺便加置信度过滤,效果才稳下来。
做法是把训练数据分成五份,每次用四份训练、一份验证,记录每个epoch在验证集上的F1。选平均F1最高的epoch作为最终训练epoch。这个过程不要自己写循环,直接用sklearn.model_selection.StratifiedKFold:
from sklearn.model_selection import StratifiedKFold skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) for fold, (train_idx, val_idx) in enumerate(skf.split(df["text"], df["label"])): # 每个fold重新构建dataset和dataloader train_df = df.iloc[train_idx] val_df = df.iloc[val_idx] print(f"fold {fold}: train {len(train_df)}, val {len(val_df)}")跑完后你会得到每个候选epoch的验证F1均值和方差。方差大的模型不要上生产,说明对新闻话题变化太敏感。
预测阶段加置信度过滤是我落地时的习惯。三分类新闻情感模型常常出现“0.45负面、0.35中性、0.20正面”这种糊在一起的输出,强制argmax会乱标。源码提供的predict函数如果不带置信度,我会改成:
def predict_with_threshold(pred_label, confidence, low_conf_threshold=0.6): if confidence < low_conf_threshold: return "不确定" return pred_label业务侧拿到“不确定”样本,先走人工复核,比自己强行给负面新闻打错标签省心得多。新闻系统里误报负面舆情和漏报一样危险。
最后聊一个习惯:我会先把项目说明文档里标注的模型效果在本地用同目录测试集复现一遍,达不到说明数值就先查数据划分,不急着调参。跑通之后再去做BERT-wwm和BERT的对比实验,把实验记录写在代码旁边的experiments/下。这样两周后回看,还能知道当初为什么把dropout设成0.3、为什么把warmup设成10%。技术方案的长期价值,恰恰在这些可复现的记录里。希望这套折腾能帮到你。
本文还有配套的精品资源,点击获取