day33nlprnn

python 复制代码
"""
实验3: TextRNN (BiLSTM + Attention) 文本分类 (exp3)
====================================================
"""
import os, json, time, random, warnings
warnings.filterwarnings('ignore')

import numpy as np
import pandas as pd
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
from sklearn.metrics import f1_score, accuracy_score, classification_report

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DATA_DIR = os.path.join(BASE_DIR, 'data')
LOGS_DIR = os.path.join(BASE_DIR, 'logs')
MODELS_DIR = os.path.join(BASE_DIR, 'models')
SUBMISSIONS_DIR = os.path.join(BASE_DIR, 'submissions')
for d in [LOGS_DIR, MODELS_DIR, SUBMISSIONS_DIR]:
    os.makedirs(d, exist_ok=True)

LABEL_MAP = {0: '科技', 1: '股票', 2: '体育', 3: '娱乐', 4: '时政',
             5: '社会', 6: '教育', 7: '财经', 8: '家居', 9: '游戏',
             10: '房产', 11: '时尚', 12: '彩票', 13: '星座'}

SEED = 42
MAX_LEN = 256
VOCAB_SIZE = 7550
EMBED_DIM = 100
# 【核心参数大换血】
HIDDEN_SIZE = 128  # LSTM 单向隐藏层维度。因为是双向(Bi),最终特征会变成 128 * 2 = 256 维。
NUM_LAYERS = 2     # 堆叠两层 LSTM,让模型能学到更深层的逻辑。
DROPOUT = 0.15
NUM_CLASSES = 14
BATCH_SIZE = 16    # 【注意】RNN 极其吃显存,所以 Batch Size 从 128 降到了 16!
LR = 2e-4
EPOCHS = 10
DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

def set_seed(seed):
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)

set_seed(SEED)


# ==========================================
# 关卡一:带有"真实长度"记录仪的数据加工厂
# ==========================================
class NewsDataset(Dataset):
    def __init__(self, texts, labels=None, max_len=MAX_LEN):
        # 【招募库管员】
        # 把所有新闻文本(长串的数字字符串)和标签存进仓库
        self.texts = texts
        self.labels = labels
        self.max_len = max_len  # 规定流水线上的标准包装盒大小(256)

    def __len__(self):
        # 【清点库存】
        # 告诉 DataLoader,我们仓库里总共有多少篇新闻
        return len(self.texts)

    def __getitem__(self, idx):
        # 【核心加工流水线】:当 DataLoader 来要第 idx 篇新闻时,现场加工

        # 1. 拆解零件:把 "345 889 12" 这种整块的字符串,切成一个个整数组成的列表
        tokens = [int(t) for t in self.texts[idx].split()]

        # ---------------------------------------------------------
        # 🌟【全场最核心的改动:记录真实长度】🌟
        # ---------------------------------------------------------
        # 为什么用 min?
        # 如果句子长度是 50,min(50, 256) = 50。真实长度就是 50。
        # 如果句子长度是 300,超过了盒子大小,一会儿要被切掉,所以它的有效真实长度最多只能算作 256。
        # 【面试防坑】:这个 length 极其重要!它要被送给后方的 LSTM,告诉它"算到这个长度就赶紧停下,后面的全是 0,不用看了!"
        length = min(len(tokens), self.max_len)
        # todo:GPU 是一个极其死板的"流水线工人",它根本不懂什么是"句子",它只认识方方正正的矩阵
        #  所以固定长度

        # 2. 截断与填充 (把残次品变成标准件)
        if len(tokens) > self.max_len:
            # 太长的,一刀切掉尾巴
            tokens = tokens[:self.max_len]
        else:
            # 太短的,屁股后面全塞上 0 (Padding)
            tokens = tokens + [0] * (self.max_len - len(tokens))

        # 3. 装车打包:转换成 PyTorch 喜欢的 Tensor (长整型)矩阵
        x = torch.tensor(tokens, dtype=torch.long)

        # 4. 发货:带着"小尾巴"出厂
        if self.labels is not None:
            # 训练集:不仅返回标准件 x,返回标签 y,还把【真实长度 length】一起打包发走!
            return x, torch.tensor(self.labels[idx], dtype=torch.long), length

        # 测试集:没有标签,但依然要带上【真实长度 length】
        return x, length


# ==========================================
# 关卡三:划重点的神器 (Attention 机制)
# ==========================================
class Attention(nn.Module):
    def __init__(self, hidden_size):
        super().__init__()
        # 【招募阅卷老师】
        # hidden_size 在这里是 256 (因为是双向 LSTM,128 * 2)

        # 1. 投影矩阵 W:相当于给阅卷老师配了一副"透视眼镜"
        self.W = nn.Linear(hidden_size, hidden_size)

        # 2. 核心评价向量 v:相当于阅卷老师脑子里的"标准答案"
        # nn.Parameter 意味着这是一个可以随着训练不断优化的参数,模型会自己学到什么特征最重要。
        self.v = nn.Parameter(torch.randn(hidden_size))

    def forward(self, h, mask=None):
        # 【开始阅卷】
        # 假设当前输入 h (Bi-LSTM的输出) 的形状是: [16, 256, 256]
        # 代表 16句话,每句话 256个词,每个词已经被提炼成了 256维 的高级特征。

        # ---------------------------------------------------------
        # 第一步:给每个词打初评分
        # ---------------------------------------------------------
        # 先让 256 维的特征戴上透视眼镜 (W),然后用 tanh 激活函数把数值压缩到 -1 到 1 之间
        score = torch.tanh(self.W(h))

        # 让处理过的特征,去和老师脑子里的标准答案 (v) 算内积 (matmul)
        # 内积越大,说明这个词的特征和标准答案越匹配,分数越高!
        # 形状突变: [16, 256, 256] 和 [256] 相乘 -> [16, 256] (每句话里的 256 个词,各自得到了一个具体的分数)
        score = torch.matmul(score, self.v)

        # ---------------------------------------------------------
        # 第二步:极其冷酷的 Mask (遮蔽废话)
        # ---------------------------------------------------------
        if mask is not None:
            # 面试官必考:为什么要填 -1e9 (负十亿)?
            # 答:因为后面要过 Softmax。补齐的 0 没有任何语义,如果初评分数是 0,过 Softmax 后依然会分走一部分权重。
            # 把补齐位置的分数强行改成 -1e9,Softmax(-1e9) 会无限趋近于绝对的 0。彻底封杀废话的发言权!
            score = score.masked_fill(~mask, -1e9)

        # ---------------------------------------------------------
        # 第三步:转化为重要性百分比 (权重分配)
        # ---------------------------------------------------------
        # softmax 会把 256 个词的分数,变成加起来等于 100% 的概率分布。
        # 比如"科技"占 80%,"发布"占 15%,标点符号占 0%。
        # unsqueeze(2) 是为了补全维度,方便后面做乘法。形状突变: [16, 256] -> [16, 256, 1]
        attn = torch.softmax(score, dim=1).unsqueeze(2)

        # ---------------------------------------------------------
        # 第四步:加权融合 (提取终极精华)
        # ---------------------------------------------------------
        # h [16, 256, 256] * attn [16, 256, 1]
        # 这一步极其优美:把每个词的 256 维特征,乘以它分配到的权重。废话乘以 0 直接消失,关键词乘以大比重被无限放大。
        # 最后用 sum(dim=1) 把这 256 个加权后的词压缩融合成 1 个向量!
        # 形状突变: [16, 256, 256] -> [16, 256]
        out = (h * attn).sum(dim=1)

        return out  # 吐出这 16 句话的终极浓缩精华!





# ==========================================
# 终极缝合怪:TextRNN (BiLSTM + Attention) 主干网络
# ==========================================
class TextRNN(nn.Module):
    def __init__(self):
        super().__init__()
        # 1. 词嵌入层 (查字典):7550个词,每个词变100维向量。遇到0(补齐位)直接输出全0。
        # 75500*100
        self.embedding = nn.Embedding(VOCAB_SIZE, EMBED_DIM, padding_idx=0)

        # 2. 核心大闸:双向 LSTM (Bi-LSTM)
        # EMBED_DIM: 输入维度(100)
        # HIDDEN_SIZE: 隐藏层维度(128)。因为开了双向(bidirectional=True),输出维度会翻倍变成 256。
        # batch_first=True: 告诉 PyTorch 输入的第一维是 batch_size [batch, seq_len, feature]
        self.lstm = nn.LSTM(EMBED_DIM, HIDDEN_SIZE, NUM_LAYERS,
                            batch_first=True, bidirectional=True,
                            dropout=DROPOUT if NUM_LAYERS > 1 else 0)

        # 3. 注意力机制 (划重点):输入是 Bi-LSTM 的输出维度 (128 * 2 = 256)
        self.attention = Attention(HIDDEN_SIZE * 2)

        # 4. 随机失活防过拟合
        self.dropout = nn.Dropout(DROPOUT)

        # 5. 最终法官 (全连接层):把提炼出的 256维 精华,映射到 14个 新闻类别上打分
        self.fc = nn.Linear(HIDDEN_SIZE * 2, NUM_CLASSES)

    def forward(self, x, lengths=None):
        # 假设 x 的形状: [16, 256] (16句话,每句强行Pad到了256长)

        # 【极其关键的 Mask 掩码生成】
        # 找出 x 里不是 0 的真实单词位置,标为 True;是 0 的补齐位,标为 False。
        # 这个 mask 后面会送给 Attention,用来屏蔽掉补齐位的注意力打分。
        mask = (x != 0)

        # 查字典:形状突变 [16, 256] -> [16, 256, 100]
        x = self.embedding(x)
        x = self.dropout(x)

        # 【面试高光时刻:动态序列打包】
        if lengths is not None:
            # 第一步:真空压缩 (剥离所有的 Padding 0)
            # 把原本方方正正带有海量 0 的矩阵,按真实长度压缩成极其紧凑的 PackedSequence 对象。
            packed = nn.utils.rnn.pack_padded_sequence(
                x, lengths.cpu().clamp(min=1), batch_first=True, enforce_sorted=False)

            # 第二步:纯净版 LSTM 计算 (速度极快,且记忆不会被 0 污染)
            out, _ = self.lstm(packed)

            # 第三步:充气解压 (把压缩包还原成方阵)
            # 此时的 out 恢复成了 [16, 256, 256]。原来补 0 的位置,现在的特征依然是 0。
            out, _ = nn.utils.rnn.pad_packed_sequence(out, batch_first=True, total_length=mask.size(1))
        else:
            # 裸考模式:如果没有提供真实长度,只能带 0 傻算(慢且效果差)
            out, _ = self.lstm(x)

        # 经过 LSTM 后,out 形状: [16, 256, 256]

        # 把带有 256 步特征的 out 和刚才生成的 mask 交给 Attention 老师划重点。
        # Attention 老师会把这 256 个词的特征,按重要性加权浓缩成 1 个特征!
        # 形状突变: [16, 256, 256] -> [16, 256]
        out = self.attention(out, mask)

        out = self.dropout(out)

        # 最终审判:输出 14 个类别的原始打分 (Logits)
        # 形状突变: [16, 256] -> [16, 14]
        return self.fc(out)


# ==========================================
# 训练引擎与评估体系 (与 TextCNN 逻辑一致,但加入了 lengths)
# ==========================================

def get_class_weights(labels):
    # 【劫富济贫】:统计各类别频次并取倒数,给样本极少的类(如星座)极大的 Loss 权重惩罚。
    counts = np.bincount(labels, minlength=NUM_CLASSES)
    weights = 1.0 / (counts + 1e-6)
    weights = weights / weights.sum() * NUM_CLASSES  # 均值归一化,防止 Loss 整体过小
    return torch.tensor(weights, dtype=torch.float32)


# 关卡五:训练引擎 (核心进化的逻辑)
# ==========================================
def train_epoch(model, loader, optimizer, criterion, device):
    # 【启动:进入战斗模式】
    # 启用 Dropout 和 Batch Normalization,允许参数被更新。
    model.train()

    total_loss = 0
    all_preds, all_labels = [], []

    # 这里的 batch 通常包含 [x, y, lengths]
    for batch in loader:
        # ---------------------------------------------------------
        # 第一步:数据搬运
        # x: [16, 256], y: [16], lengths: [16] (假设 Batch=16)
        # ---------------------------------------------------------
        x, y, lengths = batch[0].to(device), batch[1].to(device), batch[2]

        # ---------------------------------------------------------
        # 第二步:清空记忆 (Gradient Zeroing)
        # ---------------------------------------------------------
        # PyTorch 的梯度是累加的!如果不洗干净"上一个 Batch"留下的梯度,
        # 这次的更新就会乱套。就像炒新菜前必须洗锅。
        optimizer.zero_grad()

        # ---------------------------------------------------------
        # 第三步:前向传播 (Forward)
        # ---------------------------------------------------------
        # logits 形状: [16, 14] (16 句话在 14 个类别上的裁判打分)
        logits = model(x, lengths)

        # ---------------------------------------------------------
        # 第四步:计算痛苦值 (Loss Calculation)
        # ---------------------------------------------------------
        # 计算预测分数 logits 与真实标签 y 之间的差距。
        # 这里用的是交叉熵损失 (CrossEntropyLoss),包含了类别加权。
        loss = criterion(logits, y)

        # ---------------------------------------------------------
        # 第五步:反向传播与优化 (Backprop & Update)
        # ---------------------------------------------------------
        # 1. 甩锅:计算 loss 对模型每个参数的贡献(梯度)
        loss.backward()

        # 2. 安检(防止爆炸):
        # 面试必考:梯度裁剪 (Gradient Clipping)。
        # LSTM 很容易出现梯度爆炸,这行代码强行把过大的梯度限制在 5.0 以内。
        nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0)

        # 3. 进化:optimizer 顺着梯度方向,微调模型里的权重参数
        optimizer.step()

        # ---------------------------------------------------------
        # 统计环节
        # ---------------------------------------------------------
        total_loss += loss.item() * x.size(0)
        # argmax(1) 找出 14 个类里分最高的索引
        all_preds.extend(logits.argmax(1).cpu().numpy())
        all_labels.extend(y.cpu().numpy())

    # 计算整轮的平均损失和 Macro-F1 (衡量不平衡类别的核心指标)
    avg_loss = total_loss / len(loader.dataset)
    f1 = f1_score(all_labels, all_preds, average='macro')

    return avg_loss, f1


# ==========================================
# 关卡四:验证引擎 (推理与性能评估)
# ==========================================

@torch.no_grad()  # 【省钱/省显存秘籍】
# 装饰器:告诉 PyTorch 彻底关闭"梯度追踪"。
# 在验证时我们不更新参数,不存梯度能让显存占用降低 50% 以上,速度也飞快。
def eval_epoch(model, loader, criterion, device):
    model.eval()  # 【重要状态切换】
    # 将模型设为"评估模式"。这会影响 Dropout 层:
    # 训练时:Dropout 随机丢弃神经元(为了抗过拟合)。
    # 验证时:Dropout 必须关闭(100% 神经元在线),否则预测结果会随机摆动,不稳定。

    total_loss = 0
    all_preds, all_labels = [], []

    for batch in loader:
        # 1. 搬运数据到显卡
        x, y, lengths = batch[0].to(device), batch[1].to(device), batch[2]

        # 2. 前向传播:拿到那 14 个类别的"裁判评分" (Logits)
        logits = model(x, lengths)

        # 3. 计算损失:虽然不反向传播,但要看 Loss 降下来没有
        loss = criterion(logits, y)

        # 4. 统计总误差:loss.item() 拿到的单均值,要乘上当前 Batch 的样本数 x.size(0)
        total_loss += loss.item() * x.size(0)

        # 5. 记录预测结果:
        # argmax(1):在 14 个分数里找最大的那个位置(索引)。
        # .cpu().numpy():从显存里拉回到内存,变成普通的 numpy 数组方便存进列表。
        all_preds.extend(logits.argmax(1).cpu().numpy())
        all_labels.extend(y.cpu().numpy())

    # 【核心指标计算】
    # 1. 平均 Loss = 总 Loss / 样本总数
    avg_loss = total_loss / len(loader.dataset)

    # 2. Macro-F1 (宏平均 F1):
    # 由于天池数据集类别极度不平衡(科技类多,星座类极少),
    # 用 Macro-F1 能防止模型通过"只猜大类"来骗高分。它是对 14 个类分别算 F1 再取平均。
    f1 = f1_score(all_labels, all_preds, average='macro')

    # 3. Accuracy (准确率):最直观的指标,看看 100 篇新闻猜对了几个。
    acc = accuracy_score(all_labels, all_preds)

    return avg_loss, f1, acc, all_preds, all_labels


# ==========================================
# 关卡六:实战检阅 (推理/预测阶段)
# ==========================================

@torch.no_grad()  # 【只读模式】
# 告诉 PyTorch:现在是纯推理阶段,千万不要计算梯度,把求导引擎锁死。
# 这样可以极大释放显存,防止在跑几万条测试数据时显存炸掉。

def predict(model, loader, device):
    model.eval()  # 【稳定模式】
    # 必须关掉 Dropout。我们预测的时候需要模型给出最确定的判断,不能再随机丢弃神经元了。

    all_preds = []

    for batch in loader:
        # 1. 搬运原材料:把测试集的新闻编号 x 送上显卡
        x = batch[0].to(device)

        # ---------------------------------------------------------
        # 2. 核心细节:兼容性安检 (Compatibility Check)
        # ---------------------------------------------------------
        # 为什么会有这么绕的一行?
        # - 训练集:通常返回 (x, y, lengths),len(batch) 是 3
        # - 测试集:没有真实标签 y,通常只返回 (x, lengths),len(batch) 是 2
        # 这行代码保证了:无论 y 在不在,我们都能准确抓到那个关键的 lengths。
        lengths = batch[1] if len(batch) == 2 else batch[2]

        # 3. 询问模型:这几篇新闻分别是什么类?
        # logits 形状: [Batch, 14]
        logits = model(x, lengths)

        # 4. 做出最终裁决:
        # argmax(1):在 14 个分数里找最大的那个。
        # .cpu().numpy():把"裁判的判决"从显卡拉回内存,变成普通的数字数组。
        all_preds.extend(logits.argmax(1).cpu().numpy())

    return all_preds  # 返回一个长长的列表,里面全是 0~13 的数字


def main():
    # ---------------------------------------------------------
    # 1. 实验初始化与环境打印
    # ---------------------------------------------------------
    print("=" * 60)
    print(f"实验3: TextRNN BiLSTM+Attention (max_len={MAX_LEN}, device={DEVICE})")
    print("=" * 60)

    # ---------------------------------------------------------
    # 2. 数据加载:从硬盘读取 CSV 文件
    # ---------------------------------------------------------
    train_df = pd.read_csv(os.path.join(DATA_DIR, 'train_split.csv'), sep='\t')
    val_df = pd.read_csv(os.path.join(DATA_DIR, 'val_split.csv'), sep='\t')
    test_df = pd.read_csv(os.path.join(DATA_DIR, 'test_a.csv'), sep='\t')

    # ---------------------------------------------------------
    # 3. 封装 Dataset:把 DataFrame 转为 PyTorch 能识别的 Dataset 对象
    # ---------------------------------------------------------
    train_ds = NewsDataset(train_df['text'].tolist(), train_df['label'].tolist())
    val_ds = NewsDataset(val_df['text'].tolist(), val_df['label'].tolist())
    test_ds = NewsDataset(test_df['text'].tolist())  # 测试集没标签

    # ---------------------------------------------------------
    # 4. 构造 DataLoader:开启多线程读取,pin_memory 加速数据从内存到显存的拷贝
    # ---------------------------------------------------------
    train_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=4, pin_memory=True)
    val_loader = DataLoader(val_ds, batch_size=BATCH_SIZE * 2, shuffle=False, num_workers=4, pin_memory=True)
    test_loader = DataLoader(test_ds, batch_size=BATCH_SIZE * 2, shuffle=False, num_workers=4, pin_memory=True)

    # ---------------------------------------------------------
    # 5. 模型实例化:并搬运到 GPU (DEVICE)
    # ---------------------------------------------------------
    model = TextRNN().to(DEVICE)
    # 计算总参数量:看看你的显存花在哪了
    param_count = sum(p.numel() for p in model.parameters())
    print(f"模型参数量: {param_count / 1e6:.2f}M")

    # ---------------------------------------------------------
    # 6. 核心三件套:损失函数、优化器、学习率调度器
    # ---------------------------------------------------------
    # 计算类别权重:针对天池数据分布不均(科技多,星座少)进行的"补偿"
    class_weights = get_class_weights(train_df['label'].values).to(DEVICE)
    criterion = nn.CrossEntropyLoss(weight=class_weights)  # 带权重的交叉熵
    optimizer = torch.optim.Adam(model.parameters(), lr=LR)
    # 余弦退火调度器:让学习率像波浪一样下降,有助于模型跳出局部最优解
    scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS)

    best_f1 = 0
    history = []  # 用于记录每轮的 Loss 和 Acc,方便后续画图

    # ---------------------------------------------------------
    # 7. 核心训练循环 (Training Loop)
    # ---------------------------------------------------------
    for epoch in range(1, EPOCHS + 1):
        t0 = time.time()
        # 训练一轮
        train_loss, train_f1 = train_epoch(model, train_loader, optimizer, criterion, DEVICE)
        # 验证一轮
        val_loss, val_f1, val_acc, val_preds, val_labels = eval_epoch(model, val_loader, criterion, DEVICE)

        # 学习率更新
        scheduler.step()
        elapsed = time.time() - t0

        print(f"Epoch {epoch:2d}/{EPOCHS} | "
              f"train_loss={train_loss:.4f} train_f1={train_f1:.4f} | "
              f"val_loss={val_loss:.4f} val_f1={val_f1:.4f} val_acc={val_acc:.4f} | "
              f"time={elapsed:.1f}s")

        # 保存训练历史
        history.append({
            'epoch': epoch, 'train_loss': train_loss, 'train_f1': train_f1,
            'val_loss': val_loss, 'val_f1': val_f1, 'val_acc': val_acc
        })

        # 【模型存档】:如果这轮的 F1 分数最高,就存下"最强版本"
        if val_f1 > best_f1:
            best_f1 = val_f1
            torch.save(model.state_dict(), os.path.join(MODELS_DIR, 'textrnn_best.pt'))
            print(f"  -> 保存最佳模型 (val_f1={best_f1:.4f})")

    # ---------------------------------------------------------
    # 8. 最终评估:加载表现最好的那个模型权重
    # ---------------------------------------------------------
    model.load_state_dict(torch.load(os.path.join(MODELS_DIR, 'textrnn_best.pt')))
    _, final_f1, final_acc, val_preds, val_labels = eval_epoch(model, val_loader, criterion, DEVICE)

    # 打印每个类别的详细表现 (F1, Precision, Recall)
    per_class = f1_score(val_labels, val_preds, average=None)
    per_class_dict = {LABEL_MAP[i]: float(f'{v:.4f}') for i, v in enumerate(per_class)}
    print(f"\n最终验证集: macro-F1={final_f1:.4f}, accuracy={final_acc:.4f}")
    print(classification_report(val_labels, val_preds,
                                target_names=[LABEL_MAP[i] for i in range(14)], digits=4))

    # ---------------------------------------------------------
    # 9. 高阶操作:保存 Logits (为模型融合做准备)
    # ---------------------------------------------------------
    model.eval()
    val_logits_list = []
    with torch.no_grad():
        for batch in val_loader:
            x, _, lengths = batch[0].to(DEVICE), batch[1], batch[2]
            logits = model(x, lengths)
            val_logits_list.append(logits.cpu())
    # 拼接并保存验证集的原始分数(14维)
    val_logits_np = torch.cat(val_logits_list, dim=0).numpy()
    np.save(os.path.join(MODELS_DIR, 'textrnn_val_logits.npy'), val_logits_np)

    # ---------------------------------------------------------
    # 10. 测试集预测与提交文件生成
    # ---------------------------------------------------------
    test_logits_list = []
    with torch.no_grad():
        for batch in test_loader:
            x = batch[0].to(DEVICE)
            lengths = batch[1] if len(batch) == 2 else batch[2]
            logits = model(x, lengths)
            test_logits_list.append(logits.cpu())
    test_logits_np = torch.cat(test_logits_list, dim=0).numpy()
    np.save(os.path.join(MODELS_DIR, 'textrnn_test_logits.npy'), test_logits_np)

    # 从 14 维分数取最大值,生成最终 CSV
    test_preds = test_logits_np.argmax(axis=1)
    sub_df = pd.DataFrame({'label': test_preds})
    sub_df.to_csv(os.path.join(SUBMISSIONS_DIR, 'submission_textrnn.csv'), index=False)
    print(f"-> logits已保存: textrnn_val_logits.npy, textrnn_test_logits.npy")

    # ---------------------------------------------------------
    # 11. 实验日志持久化:方便复盘和写论文
    # ---------------------------------------------------------
    result = {
        'model': 'TextRNN_BiLSTM_Attention',
        'history': history,
        'per_class_f1': per_class_dict,  # 记录模型在哪几个类分得不好
        # ... 其他超参数 ...
    }
    with open(os.path.join(LOGS_DIR, 'exp3_textrnn_results.json'), 'w') as f:
        json.dump(result, f, ensure_ascii=False, indent=2)

    # 将实验关键指标追加到 experiment_log.csv
    # ... csv 保存逻辑 ...

    print("\n[完成] 实验3 TextRNN完毕!")
if __name__ == '__main__':
    main()

@浙大疏锦行

相关推荐
AC赳赳老秦1 小时前
2026国产大模型协同趋势:以DeepSeek为枢纽,构建高效智能协作网络
大数据·网络·人工智能·搜索引擎·交互·ai-native·deepseek
小鸡吃米…1 小时前
自然语言处理 ——Python 实现
人工智能·python·自然语言处理
Alex艾力的IT数字空间1 小时前
OCR 原理:从像素到文本的智能转换
数据结构·人工智能·python·神经网络·算法·cnn·ocr
FluxMelodySun2 小时前
机器学习(二十) 集成学习-Boosting与Bagging集成方法
人工智能·机器学习·集成学习
李同学Lino2 小时前
拒绝 500 元智商税!AutoClaw 零门槛安装教程,手把手教你低成本“喂龙虾”
人工智能·ai·github·openclaw·autoclaw
进击ing小白2 小时前
OpenCv之多通道的分离与合并
人工智能·opencv·计算机视觉
李建军2 小时前
OpenClaw 国内版快速安装指南 (2026 更新)
人工智能
菩提树下的凡夫2 小时前
千问VL2.5大模型+Pyside6目标检测-连载6
人工智能·目标检测·计算机视觉
星幻元宇VR2 小时前
VR生产安全学习机|开启智慧安全培训新时代
人工智能·科技·学习·安全·vr