简介:一份面向自然语言处理与影视智能化创作研究者的PDF技术文档,系统探讨多角色关系Transformer在影视剧本生成中的应用,尤其聚焦于剧情冲突设计的强化学习机制。文档共27页,以单一PDF文件呈现,压缩包大小2.11MB,支持目录章节跳转与大纲快速定位,结构清晰完整。内容从Transformer基础架构与角色关系建模展开,深入设计了关系感知的注意力机制与多角色交互模块,并探讨基于强化学习的剧情冲突设计方法,包括状态表示、动作空间、奖励函数等关键部分。后续章节涵盖融合系统实现、训练评估、消融实验及爱情悬疑、历史战争、科幻喜剧三个应用案例,能够帮助读者理解从模型设计到实际生成的全流程。已有33人学习下载,适合希望掌握多角色关系建模与强化学习在剧情生成中应用的研究人员、算法工程师及高年级学生参考。
1. 多角色关系Transformer遇上强化学习:剧情冲突设计为什么需要两套机制
自动写剧本最大的问题不是“写不出”,而是“吵不起来”。预训练语言模型天然倾向于把分歧在下一句里化解,因为人类文本里大多数对话都服从合作原则,模型学到的条件分布把转折平滑掉了。可影视剧本的冲突恰恰需要对抗持续几个场次,让角色的目标互相挤压,最后用一个小动作引爆。这里有两个本质难点:第一,冲突状态由角色之间的关系决定,关系必须在长上下文中保持稳定,这超出了普通Transformer的静态位置感知能力;第二,剧情推进是一连串决策,生成模型没有目标函数,不知道应当在第几句真正把矛盾挑明,这正好是强化学习能补上的部分。
所以常见做法是两段式:用多角色关系Transformer把“谁和谁是敌对/隐瞒/单恋”编码成注意力偏置,让生成过程明确感知关系结构;再用强化学习在带冲突奖励的剧本环境里微调策略。Transformer负责维护关系,强化学习负责把关系推向冲突。以下按我实际搭建这套方案的经验展开,重点放在关系建模、奖励设计和训练稳定上。
2. 多角色关系Transformer:把角色关系变成可计算的注意力偏置
2.1 先定义关系图:从关系列表到多维关系张量
在写任何模型代码之前,先把剧本里的角色关系结构化。假设一幕戏里有 N 个角色,角色对 (i,j) 之间可能存在多种关系。我的做法是定义一个固定关系词表,比如0=padding、1=同盟、2=敌对、3=单恋、4=隐瞒。每个场次单独维护一张N×N关系矩阵,因为剧情推进后关系会变化,不能把整本剧本当成一张静态图。
下面这段代码构造关系嵌入,并把它转换成后续注意力可以使用的偏置:
import torch import torch.nn as nn # 关系类型常量 REL_PAD = 0 REL_ALLY = 1 REL_ENEMY = 2 REL_CRUSH = 3 REL_SECRET = 4 def build_relation_bias(rel_ids, rel_embed, num_heads): """ rel_ids: [batch, num_characters, num_characters] rel_embed: nn.Embedding(num_relations, rel_dim) 返回: [batch, num_heads, num_characters, num_characters] """ rel_vec = rel_embed(rel_ids) # [B, N, N, D] rel_vec = rel_vec.view( *rel_vec.shape[:-1], num_heads, rel_vec.size(-1) // num_heads ) # 用每个关系向量在子空间里的模长作为偏置强度 bias = rel_vec.norm(dim=-1) # [B, N, N, heads] return bias.permute(0, 3, 1, 2).contiguous() # [B, heads, N, N]这段代码的关键点是mod后的向量求模长,而不是直接加一个可学习偏置。直接让模型学习关系偏置,常常会在训练初期把所有关系的偏置都推成同一个值,导致关系识别失效。用模长作为偏置并配合一个可学习的门控,可以让网络自己决定每种关系表达成“注意更多”还是“注意更少”。
不过真正推理时N个角色会对应一段 token 序列,关系矩阵维度不能直接对齐 token。我会在每个角色的台词前插入一个“角色起始 Token”,该 Token 的 hidden state 作为角色身份信息。关系偏置随后按角色段展开,变成[B, heads, seq_len, seq_len]。
2.2 关系感知注意力:偏置加到 softmax 前,而不是特征拼接
很多实现会把关系嵌入拼到 token embedding 里,但那样会污染语义表示。我更推荐在注意力 logits 上加偏置,让每个注意力头保留自己的关系过滤能力。下面是多头注意力中的一个头:
class RelationAwareAttention(nn.Module): def __init__(self, d_model, n_heads, n_relations, max_len): super().__init__() self.n_heads = n_heads self.head_dim = d_model // n_heads self.rel_embed = nn.Embedding(n_relations, self.head_dim * n_heads) self.pos_bias = nn.Parameter(torch.zeros(max_len, max_len)) # gate 控制关系偏置对整个注意力分布的注入强度 self.gate = nn.Parameter(torch.ones(1, n_heads, 1, 1) * 0.5) def forward(self, q, k, v, relation_bias): # q,k,v: [B, n_heads, seq_len, head_dim] attn_logits = torch.matmul(q, k.transpose(-2, -1)) attn_logits = attn_logits / (self.head_dim ** 0.5) attn_logits = attn_logits + self.gate * relation_bias attn_logits = attn_logits + self.pos_bias[:, :, :q.size(2), :q.size(3)] attn_weights = torch.softmax(attn_logits, dim=-1) return torch.matmul(attn_weights, v)门控gate初始值设成 0.5 很关键。直接初始化成 1 会让模型一开始被关系偏置带偏,忽略文本语义;设成 0 又会让关系信号完全没有梯度。0.5 作为中间值,让模型在训练头几个 epoch 先学会基本的语言生成,再逐步把关系信息利用起来。
角色与角色之间的“敌对”关系,通常会让模型跨越很长的距离去关注对方上一段台词里出现的实体词;而“隐瞒”则希望双方 token 的注意力不要太直接。这些通过偏置的正负方向就能表达。常见误用是把关系嵌入加到key或value上,这会让关系信息与语义信息混在一起,最后模型只能学到“关系词根”,无法承担长剧情里的角色立场稳定性。
2.3 场次级关系刷新:用掩码避免位置干扰
影视剧本天然分场,剧情冲突往往只在特定场次激烈。我一般会为每个场次维护一个关系矩阵,场次切换时复制上一场矩阵,再用一个小 GRU 更新有变化的关系对。这样做可以避免在每一帧的生成中都让模型重新推断“角色现在关系如何”,大大降低训练难度。
实现时要注意 padding 问题:不同场次角色数量不同,N要取 batch 内最大角色数,填充位置的关系类型必须设置成REL_PAD并在注意力掩码里屏蔽。下面关系类型的初始偏置参考值,是我在多次实验里得到的经验值:
| 关系类型 | 初始偏置建议 | 生成行为影响 |
|---|---|---|
| padding | -inf | 完全屏蔽无意义位置 |
| 同盟 | +0.3 | 双方台词互相承接,合作目标推进快 |
| 敌对 | +0.8 | 跨长距离关注对手,对话对抗性强 |
| 单恋 | +0.5 | 台词纠缠度高,但避免直接否定对方 |
| 隐瞒 | -0.2 | 降低直接注意力,留出潜台词和回避感 |
最后一行里的“隐瞒”设置负偏置,会让两个角色即使站在同一个场景里,也较少直接参考对方刚说过的话,这样更容易写出“顾左右而言他”的效果。如果全部关系都设同一符号,模型就会丢掉关系区分度,这是我在实际训练中踩过最多的问题。
3. 把剧情冲突设计转成强化学习:奖励函数与PPO更新
3.1 生成即决策:多角色关系Transformer 作为策略网络
要在一个已经训练好的语言模型上做冲突优化,先把生成过程看成马尔可夫决策过程。状态s_t是截止到当前步的所有 token、角色关系张量和场次标记;动作a_t是下一个 token 的采样;策略π_θ(a_t|s_t)就是前面实现的多角色关系Transformer。一个完整剧本或一个场次结束时,我们计算冲突奖励。
奖励不能只在剧本结束时给,否则训练方差太大。实践中常见的做法是使用混合粒度:在每个 token 步给很小的“即时节奏奖励”,比如当前句是否包含转折信号;在场次结束时给较大的“冲突强度奖励”和“连贯性奖励”。状态价值网络负责把稀疏的终局奖励回传到每个决策点,这比纯 REINFORCE 的方差低很多。
# 伪代码:在离线采样 batch 上计算旧策略对数概率并保存 with torch.no_grad(): old_log_probs = policy.compute_log_probs( samples, actions, relation_bias ).detach() old_values = value_net(samples, relation_bias).detach() # 每个 epoch 用小批量更新 PPO for epoch in range(ppo_epochs): for batch in make_minibatches(samples, old_log_probs, returns): log_probs, values = policy(batch.samples, batch.actions, batch.relation_bias) ratios = (log_probs - batch.old_log_probs).exp() advantages = batch.returns - values.squeeze(-1) # 标准 PPO clip ratio_clipped = torch.clamp(ratios, 1 - clip_range, 1 + clip_range) policy_loss = -torch.min( ratios * advantages, ratio_clipped * advantages ).mean()这里的relation_bias是随着状态变化重新计算的,不能当作常数缓存。很多强化学习实现默认把“状态”理解为一个向量,但这里每个状态里都含有一张关系图,所以需要让状态编码器每步重新输出当前关系矩阵,否则模型无法感知“角色刚刚撕破了脸”这个关键变化。
3.2 冲突强度奖励:不只看对立词,还要看关系变化
奖励函数是这套方案里最容易被做砸的部分。第一版我直接用“对立词数量”作为奖励,模型很快就学会反复写“不行!我不同意!”,虽然冲突词密度上去了,但剧情没有推进。后来我把奖励拆成三部分:
- 冲突行为分:检测角色是否在动作、拒绝、威胁、妥协等行为间切换;
- 情感落差分:场次开头与结尾的平均情感极性强度的差值;
- 关系变化分:如果敌对关系没有到转折点却被强行和解,给负奖励。
def conflict_reward(script_emotions, relation_matrix, prev_relation_matrix, threshold=0.3): # script_emotions: [seq_len] 每句的情感极性打分 polarity_gap = abs(script_emotions[-1] - script_emotions[0]) # 关系端到端变化惩罚 relation_delta = torch.abs(relation_matrix - prev_relation_matrix).sum() # 早和解惩罚:原本敌对的关系,在剧情前半段就变成同盟 early_peace_penalty = 0.0 if prev_relation_matrix[2, 5] == REL_ENEMY: if relation_matrix[2, 5] == REL_ALLY: early_peace_penalty = -1.5 reward = 0.5 * polarity_gap - 0.1 * relation_delta + early_peace_penalty return reward.item()上面这段是示意代码,真实项目中情感打分器可以用现成的中文情感分析模型,relation_delta应该用场次间的差分,而不是每个 token 都算,否则模型会为了避免变化惩罚而把关系冻结成一张恒等矩阵。
3.3 混合奖励权重:冲突不能杀死连贯性
只给冲突奖励会让模型牺牲语法和角色口吻一致性,所以需要混合奖励:
R = α · R_conflict + β · R_coherence + γ · R_relation我常用的初始权重是α=0.6, β=0.3, γ=0.1。R_coherence直接使用多角色关系Transformer自身在生成前后半段的负对数似然差值,值越小说明文本越连贯。权重不要在训练中一直固定,可以按照 reward 的滑动平均自动调整:冲突奖励一直很高时,把 α 降一点,让 β 相对上升。这种自动调权在训练后期尤其有用,能避免模型把剧情写成“喊口号式的对抗”。
强化学习算法通常用 PPO,因为它在小 batch 和中等规模模型上都很稳定。如果想尝试基于模型的强化学习,也可以训练一个小型奖励预测网络,输入场次前 200 个 token 输出冲突分数,用它代替真实奖励做 rollback,可以节省一半在线采样量,但会引入奖励预测偏差,需要定期用真实奖励校验。
4. 训练与调参:从监督微调到强化学习稳定落地
4.1 两阶段流程:先让关系Transformer学会正常剧本
强化学习微调之前,必须先做监督学习。这一步也叫行为克隆。用一批有角色标注的剧本数据,让多角色关系Transformer学习语言先验。我一般只训 1 个 epoch,过度训练会让模型对原数据分布过拟合,后面 RL 很难偏离出去。
python train_script_rl.py \ --base_model ./pretrained_base \ --relation_encoder ./supervised/checkpoint_last.pt \ --rl_algorithm ppo \ --qlr 1e-5 \ --kl_coef 0.05 \ --entropy_coef 0.01 \ --clip_range 0.2 \ --batch_size 8 \ --rollout_steps 512kl_coef是 RL 训练中用来约束策略不要偏离初始语言模型太远的系数。0.05 是常见起点,如果生成质量迅速滑坡,调到 0.1;如果冲突奖励上不去,下调到 0.02。entropy_coef控制探索程度,太大文本会胡言乱语,太小模型会过早收敛到重复模式。
4.2 关键参数表:先记住这几个,再按需调整
| 参数 | 推荐范围 | 作用 | 调参判断 |
|---|---|---|---|
kl_coef | 0.02 ~ 0.1 | 约束新策略离原始LM不要过远 | 生成文本不连贯就调大 |
clip_range | 0.1 ~ 0.3 | PPO 截断范围 | 训练波动大就调小 |
entropy_coef | 0.005 ~ 0.02 | 探索程度 | 文本重复就调大 |
reward_scale | 0.1 ~ 10 | 奖励整体缩放 | 价值 loss 震荡时校准 |
relation_gate_lr | 1e-6 ~ 1e-5 | 关系门控的学习率 | 偏置方差长期为 0 就调大 |
gae_lambda | 0.95 ~ 0.99 | 优势估计的折扣因子 | 需要强远程因果就用 0.99 |
relation_gate_lr是这套模型独有的参数。前面那个可学习 gate,不能跟主模型用同一个学习率,否则它会在训练前几千步就把偏置放大到几乎完全控制注意力,导致模型忽略文本内容。我的做法是把 gate 参数单独分组,用一个很小的学习率慢慢解锁关系信号。
4.3 训练崩溃排查:从日志和关系矩阵看问题
强化学习训练到一半会出现各种看似无解的崩溃,最常见的是三种。
第一,熵塌陷。生成的台词开始机械重复,比如每句都带“可恶”或“绝不”。这时看训练日志里的policy_entropy,如果它掉到 2.0 以下(对 vocab 较大的模型来说极低),优先增加entropy_coef,再考虑降低kl_coef。
第二,奖励涌洞。模型学到用“否定词+对标点”刷冲突分,但人类一眼看出逻辑不通。这需要引入二阶奖励,把“对立词是否指向同一实体”作为额外条件。攻击句子里的实体应当与上一句角色提到的实体一致。
def entity_consistency(aspect_a, aspect_b): # aspect_a, aspect_b 是句子中的核心实体向量 cos = torch.cosine_similarity(aspect_a.unsqueeze(0), aspect_b.unsqueeze(0)) return 0.5 * (1 + cos)第三,关系退化。训练后期关系矩阵所有值趋向相同。我在 TensorBoard 里定期记录relation_bias.mean和relation_bias.std。如果std连续几千步趋近 0,说明关系模块失去了作用。此时可以临时加入辅助损失,比如让所有关系的输出向量两两之间的余弦距离不低于0.1,迫使模型保留区分度。
relation_vecs = relation_embedding.weight[1:] # 去掉 padding cos_matrix = nn.functional.cosine_similarity( relation_vecs.unsqueeze(0), relation_vecs.unsqueeze(1), dim=-1 ) aux_loss = -cos_matrix[~torch.eye(cos_matrix.size(0), dtype=torch.bool)].mean()这种辅助损失可以在前 2000 步打开,等关系偏置稳定后再关掉,避免长期影响语言生成质量。训练过程中,还需要定期冻结价值网络,只更新策略网络,否则价值网络会追着变化极快的策略跑,导致优势估计失真。通常在rollout_steps加大到 1024 或者 batch 变大时,这种冻结更是必要。
5. 验证冲突质量:用动态关系偏置在推理阶段实时调节冲突强度
到了推理阶段,不需要重新训练模型就能控制冲突升级的速度。我发现一个很实用的技巧:在生成每句台词时,根据剧情已推进的轮次动态调整敌对角色之间的关系偏置。比如剧本前 1/4 让敌对关系偏置为 +0.3,中间 1/2 慢慢升到 +0.8,最后一幕升到 +1.2。这样做相当于给强化学习学出来的关系策略加了一个“时间助推器”。
def dynamic_relation_bias(relation_bias, step_idx, enemy_pairs, escalate_speed=0.01, max_escalate=0.5): # relation_bias: [1, heads, seq_len, seq_len] # enemy_pairs: 列表,包含 (角色A段起始token, 角色B段起始token) additional = torch.zeros_like(relation_bias) for start_a, start_b in enemy_pairs: additional[..., start_a:start_a+len_a, start_b:start_b+len_b] = \ min(step_idx * escalate_speed, max_escalate) return relation_bias + additional这个技巧只在推理时生效,不会影响已经训练好的策略。它的意义是让强化学习学到的“最合适冲突时机”可以被编剧手动改写,用来生成不同强度的结局,而不用每次都重训一个模型。
验证冲突质量时,我通常把自动指标和人工测试结合。自动指标要看三个值:conflict_reward的均值、coherence_score的均值、以及关系矩阵在相邻场次间的均方变化。如果冲突分高但连贯性分走低,说明奖励函数仍有空子可钻。人工测试不要直接给评审看整本剧本,而是同一个开头生成 5 个不同强度的后续片段,让评审按“冲突真实性”和“角色立场一致性”排序。最后把排序结果重新用来微调奖励模型,比凭经验调权重可靠得多。
需要注意的是,动态偏置的上限必须设置。如果max_escalate超过 1.2,注意力 softmax 几乎退化为 one-hot,两个角色只会疯狂互相注视对方,反而答非所问。限制在 0.5 左右,既能感受到冲突升温,又不至于破坏语言模型的基础生成能力,这是我在多个剧本数据集上验证出的可用边界。
本文还有配套的精品资源,点击获取