核心内容摘要
告别英伦旧梦:为何《唐顿庄园》第六季是时代落幕前最温柔的注脚?
viewPyTorch 的view() 是张量「重塑Reshape」函数用于改变张量的维度形状但不改变数据本身在多头注意力中view()的核心作用是将总隐藏维度拆分为「注意力头数 × 单头维度」实现多头并行计算核心规则tensor.view(*shape)作用将张量重塑为指定的shape要求「新形状的元素总数 原张量的元素总数」否则报错核心特性不改变张量的底层数据仅改变维度的 “视图”轻量操作无数据拷贝重塑后的张量与原张量共享内存修改一个另一个也会变支持用-1自动推导某一维度的大小仅能有一个-1importtorch# 一维张量重塑为二维xtorch.arange(
# shape(12,)元素总数12x_view1x.view(3,
# shape(3,
3×412x_view2x.view(4,-
# -1自动推导为3shape(4,
print(x_view
shape)# torch.Size([3,4])print(x_view
shape)# torch.Size([4,3])# 三维张量重塑核心元素总数不变ytorch.randn(2,6,
# 2×6×7689216y_viewy.view(2,6,12,
# 2×6×12×649216print(y_view.shape)# torch.Size([2,6,12,64])关键
注意事项报错场景新形状元素总数≠原总数 → x.view(3,
12≠15会报错-1的用法仅能指定一个-1用于自动计算维度如view(2,-1,
内存连续性若张量内存不连续如经过transpose/permute需先调用contiguous()再view否则报错多头注意力中view的核心作用将总隐藏维度d_model拆分为num_heads × d_k单头维度view()是实现这一拆分的关键完整流程如下步骤 1先明确核心参数以 BERT-base 为例batch_size2批次、seq_len6序列长度d_model768总隐藏维度、num_heads12注意力头数、d_k64单头维度76812×64输入querysshape(2,6,
经W_query线性变换后的输出。
步骤 2用view()拆分注意力头#
原始querys[batch, seq_len, d_model] [2,6,768]querystorch.randn(2,6,
#
拆分为多头[2,6,12,64]batch, seq_len, num_heads, d_kquerys_headsquerys.view(2,6,12,
print(querys_heads.shape)# torch.Size([2,6,12,64])#
转置调整维度为后续批量矩阵乘法[2,12,6,64]# 注transpose后内存不连续需contiguous()才能再viewquerys_headsquerys_heads.transpose(1,
.contiguous()print(querys_heads.shape)# torch.Size([2,12,6,64])步骤 3注意力计算后用view()合并多头# 假设注意力计算后的输出[2,12,6,64]batch, num_heads, seq_len, d_kattn_outputtorch.randn(2,12,6,
#
先转置回原维度[2,6,12,64]attn_outputattn_output.transpose(1,
.contiguous()#
合并多头[2,6,768]还原为总隐藏维度attn_output_mergedattn_output.view(2,6,
print(attn_output_merged.shape)# torch.Size([2,6,768])多头注意力完整实战代码importtorchimporttorch.nnasnnclassMultiHeadAttention(nn.Module):def__init__(self,d_model768,num_heads
:super().__init__()self.d_modeld_model self.num_headsnum_heads self.d_kd_model//num_heads# 64用//保证整除# 定义Q/K/V线性层self.W_querynn.Linear(d_model,d_model)self.W_keynn.Linear(d_model,d_model)self.W_valuenn.Linear(d_model,d_model)defforward(self,x):# x: [batch_size, seq_len, d_model] [2,6,768]batch_size,seq_lenx.shape[0],x.shape[1]#
线性变换Q/K/V均为[2,6,768]Qself.W_query(x)Kself.W_key(x)Vself.W_value(x)#
拆分为多头[2,6,12,64]QQ.view(batch_size,seq_len,self.num_heads,self.d_k)KK.view(batch_size,seq_len,self.num_heads,self.d_k)VV.view(batch_size,seq_len,self.num_heads,self.d_k)#
转置[2,12,6,64]batch, num_heads, seq_len, d_k# 必须contiguous()否则后续view会报错QQ.transpose(1,
.contiguous()KK.transpose(1,
.contiguous()VV.transpose(1,
.contiguous()#
计算注意力分数Q K^T → [2,12,6,6]K_TK.transpose(2,
# [2,12,64,6]attn_scoresQ K_T# [2,12,6,6]#
softmax归一化省略核心看viewattn_weightstorch.softmax(attn_scores,dim-
#
加权求和[2,12,6,64]attn_outputattn_weights V#
转置合并多头[2,6,768]attn_outputattn_output.transpose(1,
.contiguous()attn_outputattn_output.view(batch_size,seq_len,self.d_model)returnattn_output测试代码mhaMultiHeadAttention(d_model768,num_heads
xtorch.randn(2,6,
outputmha(x)print(output.shape)# 输出torch.Size([2,6,768])view vs reshape新手常混淆view和reshape二者均用于重塑张量但核心差异如下特性 view() reshape()内存共享 与原张量共享内存无拷贝 优先共享内存不连续则拷贝新内存内存连续性 要求张量内存连续否则报错 自动处理内存不连续无需contiguous()适用场景 内存连续的张量如线性层输出 内存不连续的张量如 transpose 后大模型开发建议若确定张量内存连续如线性层输出、原始输入用view()更高效若张量经过transpose/permute如多头注意力中的转置用reshape()无需手动contiguous()permute重排置换示例# 替代transpose后直接reshape更简洁QQ.transpose(1,
.reshape(batch_size,self.num_heads,seq_len,self.d_k)
总结view()核心作用改变张量维度形状不改变数据要求元素总数不变支持-1自动推导维度多头注意力中view()的核心用法拆分将[batch, seq_len, d_model]拆为[batch, seq_len, num_heads, d_k]合并注意力计算后将[batch, seq_len, num_heads, d_k]合并回[batch, seq_len, d_model]关键注意transpose/permute后需contiguous()才能用view()或直接用reshape()更便捷contiguouscontiguous()用于将「内存不连续」的张量转换为「内存连续」的张量保证张量的元素在内存中按维度顺序紧密排列是view()等操作的前置必要条件张量在计算机内存中是一维线性存储的“连续” 指的是张量的元素在内存中的排列顺序和按「维度顺序如从 0 维到最后一维」遍历张量得到的顺序完全一致直观示例二维张量假设有张量x torch.tensor([[1,2,3], [4,5,6]])shape(2,
连续内存布局内存中存储顺序是 1 → 2 → 3 → 4 → 5 → 6按 “行优先” 遍历先遍历 0 维再遍历 1 维若对x做转置x.T得到[[1,4], [2,5], [3,6]]转置后的张量逻辑上是 3 行 2 列但内存中仍存储为1→2→3→4→5→6PyTorch 的transpose/permute仅修改 “维度视图”不拷贝数据此时按转置后的维度遍历行优先期望顺序是1→4→2→5→3→6但内存实际顺序不符 → 转置后的张量是内存不连续的为什么张量会变得 “不连续”PyTorch 中以下操作会导致张量内存不连续核心是 “只改视图不改内存”维度变换类transpose()、permute()最常见如多头注意力中的维度交换索引 / 切片类非连续切片如x[:, ::2]、高级索引其他操作narrow()、expand()部分场景这些操作的设计初衷是 “轻量”—— 避免不必要的数据拷贝提升效率但代价是破坏了内存连续性contiguous()的核心作用contiguous()会创建一个新的内存连续的张量新张量与原张量数据相同但内存排列会按照 “当前维度顺序” 重新整理新张量与原张量不再共享内存是数据拷贝操作只有内存连续的张量才能调用view()view()要求张量元素在内存中是连续的否则无法正确重塑维度实战示例结合多头注意力的经典场景importtorch#
创建连续张量xtorch.randn(2,6,
# shape(2,6,
内存连续print(x.is_contiguous())# 输出True#
转置后内存不连续x_transx.transpose(1,
# 交换
2维shape(2,768,
print(x_trans.is_contiguous())# 输出False#
直接调用view()会报错try:x_trans.view(2,768,12,
# 768×612×60不768×6460812×384这里故意错核心看报错exceptExceptionase:print(报错,e)# 报错view size is not compatible with input tensors size and stride...#
先contiguous()再view()正常运行x_contigx_trans.contiguous()print(x_contig.is_contiguous())# 输出Truex_viewx_contig.view(2,768,12,
# 2×768×12×642×768×7681179648和2×768×69216哦修正x_trans.shape(2,768,
总元素2×768×69216view为2,768,12,
5重新来x_viewx_contig.view(2,768,12,
0.
# 故意错实际应保证总元素一致x_viewx_contig.view(2,768,12,
0.