Files
2026-07-13 12:37:18 +08:00

370 lines
17 KiB
Python

# -*- coding:utf-8 -*-
# Author: hankcs
# Date: 2020-06-15 21:22
from collections import defaultdict
from typing import Tuple, Union
import torch
from torch.nn import functional as F
from hanlp.components.parsers.ud import udify_util as util
from hanlp.layers.transformers.pt_imports import PreTrainedModel
def transformer_encode(transformer: PreTrainedModel,
input_ids,
attention_mask=None,
token_type_ids=None,
token_span=None,
layer_range: Union[int, Tuple[int, int]] = 0,
max_sequence_length=None,
average_subwords=False,
ret_raw_hidden_states=False):
"""Run transformer and pool its outputs.
Args:
transformer: A transformer model.
input_ids: Indices of subwords.
attention_mask: Mask for these subwords.
token_type_ids: Type ids for each subword.
token_span: The spans of tokens.
layer_range: The range of layers to use. Note that the 0-th layer means embedding layer, so the last 3 layers
of a 12-layer BERT will be (10, 13).
max_sequence_length: The maximum sequence length. Sequence longer than this will be handled by sliding
window.
average_subwords: ``True`` to average subword representations.
ret_raw_hidden_states: ``True`` to return hidden states of each layer.
Returns:
Pooled outputs.
"""
if max_sequence_length and input_ids.size(-1) > max_sequence_length:
# TODO: split token type ids in transformer_sliding_window if token type ids are not always 1
outputs = transformer_sliding_window(transformer, input_ids, max_pieces=max_sequence_length)
else:
if attention_mask is None:
attention_mask = input_ids.ne(0)
if transformer.config.output_hidden_states:
outputs = transformer(input_ids, attention_mask, token_type_ids)[-1]
else:
outputs = transformer(input_ids, attention_mask, token_type_ids)[0]
if transformer.config.output_hidden_states:
if isinstance(layer_range, int):
outputs = outputs[layer_range:]
else:
outputs = outputs[layer_range[0], layer_range[1]]
# Slow pick
# hs = []
# for h in outputs:
# hs.append(pick_tensor_for_each_token(h, token_span, average_subwords))
# Fast pick
if not isinstance(outputs, torch.Tensor):
x = torch.stack(outputs)
else:
x = outputs
L, B, T, F = x.size()
x = x.flatten(end_dim=1)
# tile token_span as x
if token_span is not None:
token_span = token_span.repeat(L, 1, 1)
hs = pick_tensor_for_each_token(x, token_span, average_subwords).view(L, B, -1, F)
if ret_raw_hidden_states:
return hs, outputs
return hs
else:
if ret_raw_hidden_states:
return pick_tensor_for_each_token(outputs, token_span, average_subwords), outputs
return pick_tensor_for_each_token(outputs, token_span, average_subwords)
def pick_tensor_for_each_token(h, token_span, average_subwords):
if token_span is None:
return h
if average_subwords and token_span.size(-1) > 1:
batch_size = h.size(0)
h_span = h.gather(1, token_span.view(batch_size, -1).unsqueeze(-1).expand(-1, -1, h.shape[-1]))
h_span = h_span.view(batch_size, *token_span.shape[1:], -1)
n_sub_tokens = token_span.ne(0)
n_sub_tokens[:, 0, 0] = True
h_span = (h_span * n_sub_tokens.unsqueeze(-1)).sum(2)
n_sub_tokens = n_sub_tokens.sum(-1).unsqueeze(-1)
zero_mask = n_sub_tokens == 0
if torch.any(zero_mask):
n_sub_tokens[zero_mask] = 1 # avoid dividing by zero
embed = h_span / n_sub_tokens
else:
embed = h.gather(1, token_span[:, :, 0].unsqueeze(-1).expand(-1, -1, h.size(-1)))
return embed
def transformer_sliding_window(transformer: PreTrainedModel,
input_ids: torch.LongTensor,
input_mask=None,
offsets: torch.LongTensor = None,
token_type_ids: torch.LongTensor = None,
max_pieces=512,
start_tokens: int = 1,
end_tokens: int = 1,
ret_cls=None,
) -> torch.Tensor:
"""
Args:
transformer:
input_ids: torch.LongTensor:
input_mask: (Default value = None)
offsets: torch.LongTensor: (Default value = None)
token_type_ids: torch.LongTensor: (Default value = None)
max_pieces: (Default value = 512)
start_tokens: int: (Default value = 1)
end_tokens: int: (Default value = 1)
ret_cls: (Default value = None)
Returns:
"""
# pylint: disable=arguments-differ
batch_size, full_seq_len = input_ids.size(0), input_ids.size(-1)
initial_dims = list(input_ids.shape[:-1])
# The embedder may receive an input tensor that has a sequence length longer than can
# be fit. In that case, we should expect the wordpiece indexer to create padded windows
# of length `max_pieces` for us, and have them concatenated into one long sequence.
# E.g., "[CLS] I went to the [SEP] [CLS] to the store to [SEP] ..."
# We can then split the sequence into sub-sequences of that length, and concatenate them
# along the batch dimension so we effectively have one huge batch of partial sentences.
# This can then be fed into BERT without any sentence length issues. Keep in mind
# that the memory consumption can dramatically increase for large batches with extremely
# long sentences.
needs_split = full_seq_len > max_pieces
if needs_split:
input_ids = split_to_sliding_window(input_ids, max_pieces)
# if token_type_ids is None:
# token_type_ids = torch.zeros_like(input_ids)
if input_mask is None:
input_mask = (input_ids != 0).long()
# input_ids may have extra dimensions, so we reshape down to 2-d
# before calling the BERT model and then reshape back at the end.
outputs = transformer(input_ids=util.combine_initial_dims_to_1d_or_2d(input_ids),
# token_type_ids=util.combine_initial_dims_to_1d_or_2d(token_type_ids),
attention_mask=util.combine_initial_dims_to_1d_or_2d(input_mask)).to_tuple()
if len(outputs) == 3:
all_encoder_layers = outputs.hidden_states
all_encoder_layers = torch.stack(all_encoder_layers)
elif len(outputs) == 2:
all_encoder_layers, _ = outputs[:2]
else:
all_encoder_layers = outputs[0]
if needs_split:
if ret_cls is not None:
cls_mask = input_ids[:, 0] == input_ids[0][0]
cls_hidden = all_encoder_layers[:, 0, :]
if ret_cls == 'max':
cls_hidden[~cls_mask] = -1e20
else:
cls_hidden[~cls_mask] = 0
cls_mask = cls_mask.view(-1, batch_size).transpose(0, 1)
cls_hidden = cls_hidden.reshape(cls_mask.size(1), batch_size, -1).transpose(0, 1)
if ret_cls == 'max':
cls_hidden = cls_hidden.max(1)[0]
elif ret_cls == 'raw':
return cls_hidden, cls_mask
else:
cls_hidden = torch.sum(cls_hidden, dim=1)
cls_hidden /= torch.sum(cls_mask, dim=1, keepdim=True)
return cls_hidden
else:
recombined_embeddings, select_indices = restore_from_sliding_window(all_encoder_layers, batch_size,
max_pieces, full_seq_len, start_tokens,
end_tokens)
initial_dims.append(len(select_indices))
else:
recombined_embeddings = all_encoder_layers
# Recombine the outputs of all layers
# (layers, batch_size * d1 * ... * dn, sequence_length, embedding_dim)
# recombined = torch.cat(combined, dim=2)
# input_mask = (recombined_embeddings != 0).long()
# At this point, mix is (batch_size * d1 * ... * dn, sequence_length, embedding_dim)
if offsets is None:
# Resize to (batch_size, d1, ..., dn, sequence_length, embedding_dim)
dims = initial_dims if needs_split else input_ids.size()
layers = util.uncombine_initial_dims(recombined_embeddings, dims)
else:
# offsets is (batch_size, d1, ..., dn, orig_sequence_length)
offsets2d = util.combine_initial_dims_to_1d_or_2d(offsets)
# now offsets is (batch_size * d1 * ... * dn, orig_sequence_length)
range_vector = util.get_range_vector(offsets2d.size(0),
device=util.get_device_of(recombined_embeddings)).unsqueeze(1)
# selected embeddings is also (batch_size * d1 * ... * dn, orig_sequence_length)
selected_embeddings = recombined_embeddings[:, range_vector, offsets2d]
layers = util.uncombine_initial_dims(selected_embeddings, offsets.size())
return layers
def split_to_sliding_window(input_ids, max_pieces):
# Split the flattened list by the window size, `max_pieces`
split_input_ids = list(input_ids.split(max_pieces, dim=-1))
# We want all sequences to be the same length, so pad the last sequence
last_window_size = split_input_ids[-1].size(-1)
padding_amount = max_pieces - last_window_size
split_input_ids[-1] = F.pad(split_input_ids[-1], pad=[0, padding_amount], value=0)
# Now combine the sequences along the batch dimension
input_ids = torch.cat(split_input_ids, dim=0)
return input_ids
def restore_from_sliding_window(all_encoder_layers, batch_size, max_pieces, full_seq_len, start_tokens, end_tokens):
# First, unpack the output embeddings into one long sequence again
unpacked_embeddings = torch.split(all_encoder_layers, batch_size, dim=-3)
unpacked_embeddings = torch.cat(unpacked_embeddings, dim=-2)
# Next, select indices of the sequence such that it will result in embeddings representing the original
# sentence. To capture maximal context, the indices will be the middle part of each embedded window
# sub-sequence (plus any leftover start and final edge windows), e.g.,
# 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
# "[CLS] I went to the very fine [SEP] [CLS] the very fine store to eat [SEP]"
# with max_pieces = 8 should produce max context indices [2, 3, 4, 10, 11, 12] with additional start
# and final windows with indices [0, 1] and [14, 15] respectively.
# Find the stride as half the max pieces, ignoring the special start and end tokens
# Calculate an offset to extract the centermost embeddings of each window
stride = (max_pieces - start_tokens - end_tokens) // 2
stride_offset = stride // 2 + start_tokens
first_window = list(range(stride_offset))
max_context_windows = [i for i in range(full_seq_len)
if stride_offset - 1 < i % max_pieces < stride_offset + stride]
final_window_start = max_context_windows[-1] + 1
final_window = list(range(final_window_start, full_seq_len))
select_indices = first_window + max_context_windows + final_window
select_indices = torch.LongTensor(select_indices).to(unpacked_embeddings.device)
recombined_embeddings = unpacked_embeddings.index_select(-2, select_indices)
return recombined_embeddings, select_indices
def build_optimizer_for_pretrained(model: torch.nn.Module,
pretrained: torch.nn.Module,
lr=1e-5,
weight_decay=0.01,
eps=1e-8,
transformer_lr=None,
transformer_weight_decay=None,
no_decay=('bias', 'LayerNorm.bias', 'LayerNorm.weight'),
**kwargs):
if transformer_lr is None:
transformer_lr = lr
if transformer_weight_decay is None:
transformer_weight_decay = weight_decay
params = defaultdict(lambda: defaultdict(list))
pretrained = set(pretrained.parameters())
if isinstance(no_decay, tuple):
def no_decay_fn(name):
return any(nd in name for nd in no_decay)
else:
assert callable(no_decay), 'no_decay has to be callable or a tuple of str'
no_decay_fn = no_decay
for n, p in model.named_parameters():
is_pretrained = 'pretrained' if p in pretrained else 'non_pretrained'
is_no_decay = 'no_decay' if no_decay_fn(n) else 'decay'
params[is_pretrained][is_no_decay].append(p)
grouped_parameters = [
{'params': params['pretrained']['decay'], 'weight_decay': transformer_weight_decay, 'lr': transformer_lr},
{'params': params['pretrained']['no_decay'], 'weight_decay': 0.0, 'lr': transformer_lr},
{'params': params['non_pretrained']['decay'], 'weight_decay': weight_decay, 'lr': lr},
{'params': params['non_pretrained']['no_decay'], 'weight_decay': 0.0, 'lr': lr},
]
from transformers import optimization
return optimization.AdamW(
grouped_parameters,
lr=lr,
weight_decay=weight_decay,
eps=eps,
no_deprecation_warning=True, # For backwards compatability
**kwargs)
def build_optimizer_scheduler_with_transformer(model: torch.nn.Module,
transformer: torch.nn.Module,
lr: float,
transformer_lr: float,
num_training_steps: int,
warmup_steps: Union[float, int],
weight_decay: float,
adam_epsilon: float,
no_decay=('bias', 'LayerNorm.bias', 'LayerNorm.weight')):
optimizer = build_optimizer_for_pretrained(model,
transformer,
lr,
weight_decay,
eps=adam_epsilon,
transformer_lr=transformer_lr,
no_decay=no_decay)
if isinstance(warmup_steps, float):
assert 0 < warmup_steps < 1, 'warmup_steps has to fall in range (0, 1) when it is float.'
warmup_steps = num_training_steps * warmup_steps
from transformers import optimization
scheduler = optimization.get_linear_schedule_with_warmup(optimizer, warmup_steps, num_training_steps)
return optimizer, scheduler
def get_optimizers(
model: torch.nn.Module,
num_training_steps: int,
learning_rate=5e-5,
adam_epsilon=1e-8,
weight_decay=0.0,
warmup_steps=0.1,
) -> Tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR]:
"""
Modified from https://github.com/huggingface/transformers/blob/7b75aa9fa55bee577e2c7403301ed31103125a35/src/transformers/trainer.py#L232
Setup the optimizer and the learning rate scheduler.
We provide a reasonable default that works well.
"""
if isinstance(warmup_steps, float):
assert 0 < warmup_steps < 1
warmup_steps = int(num_training_steps * warmup_steps)
# Prepare optimizer and schedule (linear warmup and decay)
no_decay = ["bias", "LayerNorm.weight"]
optimizer_grouped_parameters = [
{
"params": [p for n, p in model.named_parameters() if not any(nd in n for nd in no_decay)],
"weight_decay": weight_decay,
},
{
"params": [p for n, p in model.named_parameters() if any(nd in n for nd in no_decay)],
"weight_decay": 0.0,
},
]
from transformers import AdamW, get_linear_schedule_with_warmup
optimizer = AdamW(optimizer_grouped_parameters, lr=learning_rate, eps=adam_epsilon)
scheduler = get_linear_schedule_with_warmup(
optimizer, num_warmup_steps=warmup_steps, num_training_steps=num_training_steps
)
return optimizer, scheduler
def collect_decay_params(model, weight_decay):
no_decay = ["bias", "LayerNorm.weight"]
optimizer_grouped_parameters = [
{
"params": [p for n, p in model.named_parameters() if not any(nd in n for nd in no_decay)],
"weight_decay": weight_decay,
},
{
"params": [p for n, p in model.named_parameters() if any(nd in n for nd in no_decay)],
"weight_decay": 0.0,
},
]
return optimizer_grouped_parameters