当前位置: 首页 > news >正文

【实战】自然语言处理--长文本分类(3)HAN算法

HAN算法

1. 算法定义

HAN 是一种专门用于文档级文本分类的深度学习模型,通过“先词后句”的层次结构和注意力机制,自动学习出对分类最重要的词和句子表示。

  • 层次化:模拟人类阅读理解流程,先关注词—>形成句子表示,再关注句子—>形成文档表示。
  • 注意力:在每一层对不同粒度(词/句子)分配可学习的权重,以突出关键成分。

2. 算法原理

  1. 分层表示

    • 词级表示:先对句子中每个词做嵌入,然后通过双向 GRU(或 LSTM)编码上下文信息,得到每个词的上下文向量。
    • 句子级表示:再对句子表示序列(由词级注意力池化得到)做双向 GRU 编码,得到每个句子的上下文向量。
  2. 注意力机制

    • 词级注意力:对词级上下文向量进行两层全连通映射,计算得到每个词的注意力权重,池化为句子向量。
    • 句子级注意力:同理,对句子级上下文向量计算注意力权重,池化为文档向量。
  3. 分类

    • 将最终的文档向量输入全连接层,输出类别分布。

3. 模型结构示意

文档(多句)  └─ 句子i  └─ 词j  └─ Embedding  └─ BiGRU  └─ 词级注意力池化 → 句子向量  └─ 句子级 BiGRU  └─ 句子级注意力池化 → 文档向量  └─ Dropout → 全连接 → Softmax → 类别概率

4. 关键模型代码

class MyHAN(nn.Module):def __init__(...):# 词嵌入self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=pad_id)# 词级 BiGRU + 注意力self.word_gru = nn.GRU(embedding_dim, hidden_size, bidirectional=True, batch_first=True)self.word_attn_fc = nn.Linear(2*hidden_size, hidden_size)self.word_ctx    = nn.Linear(hidden_size, 1, bias=False)# 句子级 BiGRU + 注意力self.sent_gru = nn.GRU(2*hidden_size, hidden_size, bidirectional=True, batch_first=True)self.sent_attn_fc = nn.Linear(2*hidden_size, hidden_size)self.sent_ctx    = nn.Linear(hidden_size, 1, bias=False)# 输出层self.classifier = nn.Linear(2*hidden_size, num_classes)def forward(self, x):# x: [B, S, W]B, S, W = x.size()# 词级处理x = self.embedding(x).view(B*S, W, -1)            # → [B*S, W, emb]H_w, _ = self.word_gru(x)                         # → [B*S, W, 2H]u_w    = torch.tanh(self.word_attn_fc(H_w))       # → [B*S, W, H]a_w    = F.softmax(self.word_ctx(u_w), dim=1)     # → [B*S, W, 1]s      = torch.bmm(a_w.transpose(1,2), H_w)       # → [B*S, 1, 2H] → [B, S, 2H]# 句子级处理H_s, _ = self.sent_gru(s)                         # → [B, S, 2H]u_s    = torch.tanh(self.sent_attn_fc(H_s))       # → [B, S, H]a_s    = F.softmax(self.sent_ctx(u_s), dim=1)     # → [B, S, 1]v      = torch.bmm(a_s.transpose(1,2), H_s).squeeze(1)  # → [B, 2H]# 分类out    = self.classifier(v)                       # → [B, num_classes]return out

5. 训练过程

  1. 数据准备

    • 分句、分词、去停用词
    • 构建/加载词表,将每个句子填充/截断到固定长度 sent_maxlen,文档填充/截断到 doc_maxlen
  2. DataLoader

    • get_loader('train',True)get_loader('val',False) 返回迭代器
  3. 优化配置

    • 损失函数:CrossEntropyLoss
    • 优化器:AdamW(权重衰减防过拟合)
    • 学习率调度:分段衰减(Epoch < 0.3EPOCHS → 1.0;0.3–0.6 → 0.5;> 0.6 → 0.1)
  4. 训练循环

    for epoch in 1…E:for batch in train_loader:forward → 损失 → backward → 更新参数for batch in val_loader:forward → 计算验证损失/准确率保存最佳模型;记录并绘制训练/验证曲线
    
  5. 输出

    • 最佳模型权重 .pt
    • training_history.csv(记录每个 epoch 的 loss/acc)
    • han_loss.pnghan_acc.png(训练 & 验证曲线)

6. 算法作用与优势

  • 长文本适应性:分层处理避免一次性 RNN 长序列导致的梯度消失/爆炸。
  • 可解释性:注意力权重可视化,能够展示哪些词和句子对分类贡献最大。
  • 性能稳定:实验表明 HAN 在多种文档分类任务上超越传统 CNN/RNN。

代码分析

class MyHAN(nn.Module):"""Hierarchical Attention Network for document classification.Args:max_word_num (int): 最大词数 (句子长度)max_sents_num (int): 最大句子数 (文档长度)vocab_size (int): 词表大小hidden_size (int): GRU 隐藏单元维度num_classes (int): 分类数目embedding_dim (int): 词嵌入维度dropout_p (float): dropout 概率"""def __init__(self,max_word_num: int,max_sents_num: int,vocab_size: int,hidden_size: int,num_classes: int,embedding_dim: int,dropout_p: float = 0.5):super(MyHAN, self).__init__()self.max_word_num = max_word_numself.max_sents_num = max_sents_numself.hidden_size = hidden_sizeself.num_classes = num_classesself.embedding_dim = embedding_dim# 词嵌入层self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=pad_id)self.dropout_embed = nn.Dropout(dropout_p)# 词级别双向 GRU & 注意力self.word_gru = nn.GRU(input_size=embedding_dim,hidden_size=hidden_size,bidirectional=True,batch_first=True,dropout=0.2 if dropout_p < 1 else 0.0)self.word_attn_fc = nn.Linear(2 * hidden_size, hidden_size)self.word_context_vector = nn.Linear(hidden_size, 1, bias=False)# 句子级别双向 GRU & 注意力self.sent_gru = nn.GRU(input_size=2 * hidden_size,hidden_size=hidden_size,bidirectional=True,batch_first=True,dropout=0.2 if dropout_p < 1 else 0.0)self.sent_attn_fc = nn.Linear(2 * hidden_size, hidden_size)self.sent_context_vector = nn.Linear(hidden_size, 1, bias=False)# 文档级输出self.dropout = nn.Dropout(dropout_p)self.classifier = nn.Linear(2 * hidden_size, num_classes)def forward(self, x: torch.Tensor) -> torch.Tensor:# x: [batch, max_sents_num, max_word_num]batch_size, S, W = x.size()# 词嵌入x = self.embedding(x)               # [batch, S, W, emb]x = self.dropout_embed(x)x = x.view(batch_size * S, W, self.embedding_dim)# 词级别 GRUself.word_gru.flatten_parameters()H_w, _ = self.word_gru(x)          # [batch*S, W, 2*hidden]# 注意力权重u_w = torch.tanh(self.word_attn_fc(H_w))  # [batch*S, W, hidden]a_w = self.word_context_vector(u_w)      # [batch*S, W, 1]a_w = F.softmax(a_w, dim=1).transpose(1, 2)  # [batch*S, 1, W]# 句子向量s = torch.bmm(a_w, H_w).squeeze(1)         # [batch*S, 2*hidden]s = s.view(batch_size, S, 2 * self.hidden_size)# 句子级别 GRUself.sent_gru.flatten_parameters()H_s, _ = self.sent_gru(s)         # [batch, S, 2*hidden]# 注意力权重u_s = torch.tanh(self.sent_attn_fc(H_s))  # [batch, S, hidden]a_s = self.sent_context_vector(u_s)      # [batch, S, 1]a_s = F.softmax(a_s, dim=1).transpose(1, 2)  # [batch, 1, S]# 文档向量v = torch.bmm(a_s, H_s).squeeze(1)        # [batch, 2*hidden]# 分类out = self.classifier(self.dropout(v))   # [batch, num_classes]return out

1. 类定义与参数

class MyHAN(nn.Module):"""Hierarchical Attention Network for document classification.Args:max_word_num (int): 最大词数 (句子长度)max_sents_num (int): 最大句子数 (文档长度)vocab_size (int): 词表大小hidden_size (int): GRU 隐藏单元维度num_classes (int): 分类数目embedding_dim (int): 词嵌入维度dropout_p (float): dropout 概率"""
  • max_word_num(W)
    每个句子的最大词数,用于固定输入长度。
  • max_sents_num(S)
    每篇文档的最大句子数,同样用于固定层次输入。
  • vocab_size、embedding_dim
    用于构造词嵌入矩阵。
  • hidden_size
    GRU 隐藏层的维度,注意是单向的维度,双向后实际输出维度为 2*hidden_size
  • num_classes
    最终分类的类别个数。
  • dropout_p
    各层 dropout 率,帮助防止过拟合。

2. 构造函数 __init__

# 词嵌入 + Dropout
self.embedding       = nn.Embedding(vocab_size, embedding_dim, padding_idx=pad_id)
self.dropout_embed   = nn.Dropout(dropout_p)
  • Embedding:将词索引映射到稠密向量空间;padding_idx 保证填充位置不更新。
  • dropout_embed:对嵌入后向量进行 dropout。
# 词级别双向 GRU
self.word_gru        = nn.GRU(input_size=embedding_dim,hidden_size=hidden_size,bidirectional=True,batch_first=True,dropout=0.2 if dropout_p < 1 else 0.0
)
# 词级注意力:先映射到 hidden_size 空间,再投影到标量
self.word_attn_fc        = nn.Linear(2 * hidden_size, hidden_size)
self.word_context_vector = nn.Linear(hidden_size, 1, bias=False)
  • word_gru:对每个句子的词序列做双向编码,输出维度 [batch⋅S, W, 2*hidden_size]
  • word_attn_fc:将每个词的上下文向量投影到 attention 空间。
  • word_context_vector:将 attention 空间向量映射到一个标量权重。
# 句子级别双向 GRU
self.sent_gru         = nn.GRU(input_size=2 * hidden_size,hidden_size=hidden_size,bidirectional=True,batch_first=True,dropout=0.2 if dropout_p < 1 else 0.0
)
self.sent_attn_fc        = nn.Linear(2 * hidden_size, hidden_size)
self.sent_context_vector = nn.Linear(hidden_size, 1, bias=False)
  • 与词级几乎相同,区别在于输入维度是词级编码后的 2*hidden_size,输出形状 [batch, S, 2*hidden_size]
# 文档级输出层
self.dropout     = nn.Dropout(dropout_p)
self.classifier  = nn.Linear(2 * hidden_size, num_classes)
  • dropout:在最终文档向量上再做一次随机失活。
  • classifier:将 2*hidden_size 维的文档表示映射为类别 logits。

3. 前向传播 forward

batch_size, S, W = x.size()  # x.shape = [B, S, W]
  • B:批大小
  • S:句子数
  • W:每句词数

3.1 词嵌入与重排

x = self.embedding(x)        # → [B, S, W, emb]
x = self.dropout_embed(x)
x = x.view(batch_size * S, W, self.embedding_dim)
  • 嵌入后重塑为 [B⋅S, W, emb],方便送入同一个 GRU 做词级编码。

3.2 词级别双向 GRU

self.word_gru.flatten_parameters()
H_w, _ = self.word_gru(x)    # → [B⋅S, W, 2*hidden_size]
  • flatten_parameters():优化多卡/多线程时 GRU 权重布局。
  • H_w 每个词都有一个上下文表示。

3.3 词级注意力池化

u_w = torch.tanh(self.word_attn_fc(H_w))      # → [B⋅S, W, hidden_size]
a_w = self.word_context_vector(u_w)           # → [B⋅S, W, 1]
a_w = F.softmax(a_w, dim=1).transpose(1, 2)   # → [B⋅S, 1, W]
s   = torch.bmm(a_w, H_w).squeeze(1)          # → [B⋅S, 2*hidden_size]
s   = s.view(batch_size, S, 2*self.hidden_size)
  1. 映射H_wu_w(tanh 激活)
  2. 打分u_wa_w(标量注意力分数)
  3. 归一化:按词位置做 softmax
  4. 加权和:得到每个句子的固定维度向量
  5. 恢复层次:重塑回 [B, S, 2*hidden_size]

3.4 句子级别双向 GRU

self.sent_gru.flatten_parameters()
H_s, _ = self.sent_gru(s)   # → [B, S, 2*hidden_size]
  • 将句子向量序列编码为上下文相关的句子表示。

3.5 句子级注意力池化

u_s = torch.tanh(self.sent_attn_fc(H_s))       # → [B, S, hidden_size]
a_s = self.sent_context_vector(u_s)            # → [B, S, 1]
a_s = F.softmax(a_s, dim=1).transpose(1, 2)    # → [B, 1, S]
v   = torch.bmm(a_s, H_s).squeeze(1)           # → [B, 2*hidden_size]
  • 与词级注意力类似:计算每个句子的权重并做加权和,得到文档级向量 v

3.6 分类输出

out = self.classifier(self.dropout(v))  # → [B, num_classes]
return out
  • 对文档表示 v 做 dropout 再线性变换,输出各类别的对数概率(logits)。

补充问题

词级别和句级别不同?

示例回顾
  • 文档 0:“今天天气很好。我喜欢散步。晚上吃饭。”
  • 文档 1:“机器学习有趣”
  • 词表索引同前:
    今天天气→2, 好→3, 我→4, 喜欢→5, 散步→6, 晚上→7, 吃饭→8, 机器学习→9, 有趣→10, <pad>→1

参数:

  • doc_maxlen = 3(最多 3 句)
  • sent_maxlen = 4(每句 4 词)
  • 批大小 B = 2
1. 层次化输入 [B, S, W] = [2, 3, 4]
batch_tensor =
[# 文档 0[[2, 3, 1, 1],   # 今天天气 好 <pad> <pad>[4, 5, 6, 1],   # 我 喜欢 散步 <pad>[7, 8, 1, 1],   # 晚上 吃饭 <pad> <pad>],# 文档 1[[9,10, 1, 1],   # 机器学习 有趣 <pad> <pad>[1, 1, 1, 1],   # <pad> ×4[1, 1, 1, 1],   # <pad> ×4]
]
  • 第 1 维(B)是文档
  • 第 2 维(S)是句子
  • 第 3 维(W)是词

模型先按句子(W 维)做词级 BiGRU + 注意力,然后按文档(S 维)做句子级 BiGRU + 注意力。

2. 普通词级平铺输入 [B, L]

常见做法是把整个文档看成一串词,拼成定长序列,比如 L = doc_maxlen * sent_maxlen = 12

# 对文档 0:“今天天气 很好 我 喜欢 散步 晚上 吃饭”
flat0 = [2,3,4,5,6,7,8]    # 共 7 词
# pad 到长度 12:
flat0_padded = [2,3,4,5,6,7,8, 1,1,1,1,1]# 对文档 1:“机器学习 有趣”
flat1 = [9,10]
flat1_padded = [9,10, 1,1,1,1,1,1,1,1,1,1]

合成批次:

flat_batch = [[2,3,4,5,6,7,8,1,1,1,1,1],[9,10,1,1,1,1,1,1,1,1,1,1]
]  # shape [2,12]
  • 一层 BiGRU/LSTM + 注意力:直接对长度为 12 的序列编码。
  • 缺点
    1. 丢失层次信息:无法区分句子边界,注意力只能在词级别分配。
    2. 长序列梯度问题:文档很长时,单层 RNN 易梯度消失/爆炸。
  • 优点:实现简洁、输入结构单一。
对比总结
特性层次化 HAN平铺词级 RNN/CNN
输入形状[B, S, W][B, L]
模型架构词级 + 句子级 双层 GRU + 注意力单层 GRU/CNN + 注意力/池化
参数共享句内、句间分别有不同参数所有词使用同一组参数
可解释性词和句子层面都可视化注意力只有词层面注意力
长文本处理分段编码更稳定长度 L 大时训练不稳定

句子级别的优势?

在 HAN 中引入“句子级”这一层,主要是为了解决纯词级模型在长文档处理和语义抽象层面上的局限。下面从几个角度说明句子级别相较于仅词级别的优势:

  1. 减少序列长度,缓解梯度问题

    • 纯词级模型需要把整篇文档当成一条长序列(长度可能几百、上千词)来编码。
    • RNN/LSTM/GRU 在处理极长序列时容易出现梯度消失或梯度爆炸,训练不稳定。
    • HAN 先在“句子内部”做词级编码,把每一句浓缩成一个固定维度的句子向量;然后只在这些句子向量上再做一次 RNN,序列长度从“词数”降到“句子数”,大大缩短了第二层的序列长度,提升了模型对长文本的训练稳定性。
  2. 层次化语义抽象

    • 语言本身是层次化的:词组成短语/句子,句子组成段落/文档。
    • 词级注意力只能告诉你“哪些词重要”,但无法告诉你“哪些句子重要”。
    • 加入句子级后,模型能够先在词层面提取重要信息,再在句子层面进一步抽象、筛选出核心句子,实现二次语义筛选,得到更精准的文档表示。
  3. 更强的可解释性

    • 词级注意力权重告诉我们“句子里哪些词最关键”;句子级注意力权重则告诉我们“哪些句子对最终分类贡献最大”。
    • 这种双层注意力可视化,使得模型不仅划出关键词,还能帮我们定位核心段落/句子,便于人机交互和结果验证。
  4. 参数与计算的有效分离

    • 词级 GRU 和句子级 GRU 分工明确:前者关注同一句内部的短程依赖,后者关注句与句之间的长程依赖。
    • 这样一来,同样的模型容量下,比起单层超长序列的编码,层次化结构能够更高效地分配参数资源,也更容易捕捉不同层面的特征。
  5. 适应不同粒度的任务需求

    • 对于需要精细定位关键句子的应用(如文档摘要、观点抽取),句子级注意力能直接告诉我们要点句。
    • 对于需要全文整体理解的任务(情感分析、主题分类),词级和句子级结合能更全面地融合各层面信息。
http://www.dtcms.com/a/569168.html

相关文章:

  • 中国建设工程招投网站网站后台登陆口
  • 学校网站建设招聘电商推广计划
  • Ubuntu 20.04 系统库管理详细教程
  • [jmeter-商城测试]
  • Kubernetes包管理利器:Helm核心功能与架构解析指南
  • 17、docker-macvlan-1-理论
  • Mac M系列芯片制作Oracle19镜像使用docker-compose运行
  • Linux source命令详解与应用场景
  • Verilog学习 有限状态机
  • 企业网站备案审核需要多长时间沧州大型企业网站建设
  • Figma高效开发工具链:从设计到测试的完整解决方案
  • React(二):构建一个简单的聊天助手学到的React知识
  • seo优化网站的注意事项北京网络职业学院
  • JWT的说明和使用
  • MFC - 使用 Base64 对图片进行加密解密
  • Git+SSH 实现控制分支的提交权限
  • 网站建设选择题网站的内容建设
  • 怎么用自己电脑做网站服务器刚做淘客没有网站
  • CUDA C++编程指南(3.1)——使用NVCC编译
  • Numpy学习总结
  • 可梦AI获首批企业好评,蜜糖网络入驻共启AI短剧工业化
  • 笔记跨设备无缝切换?Joplin+cpolar让多设备同步更自由
  • Swift 6.2 列传(第四篇):enumerated () 的 “集合神功”
  • PDF 全文翻译开发实现思路:挑战、细节与工程化解决方案
  • 算法解析:从杨辉三角到几何查询的编程实践
  • 数学基础---刚体变换(旋转矩阵与平移矩阵)
  • 找别人建网站去哪里设计网址合集
  • 宁波网站建设费用wordpress启用注册
  • 我的第一个开源项目IOT-Tree Server-实际项目使用介绍
  • 蓝牙钥匙 第41次 紧急情况处理场景下的汽车数字钥匙系统:全方位应急方案设计与实现