Files
2026-07-13 12:40:42 +08:00

719 lines
22 KiB
Python

# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import collections
import random
import unittest
import numpy as np
from op_test import get_device_place, is_custom_device
import paddle
from paddle import Model, base, nn, set_device
from paddle.base import layers
from paddle.base.data_feeder import convert_dtype
from paddle.nn import (
RNN,
BeamSearchDecoder,
Embedding,
Layer,
Linear,
LSTMCell,
SimpleRNNCell,
dynamic_decode,
)
from paddle.static import InputSpec as Input
paddle.enable_static()
class PolicyGradient:
"""policy gradient"""
def __init__(self, lr=None):
self.lr = lr
def learn(self, act_prob, action, reward, length=None):
"""
update policy model self.model with policy gradient algorithm
"""
self.reward = paddle.static.py_func(
func=reward_func, x=[action, length], out=reward
)
neg_log_prob = paddle.nn.functional.cross_entropy(
act_prob, action, reduction='none', use_softmax=False
)
cost = neg_log_prob * reward
cost = (
(paddle.sum(cost) / paddle.sum(length))
if length is not None
else paddle.mean(cost)
)
optimizer = paddle.optimizer.Adam(self.lr)
optimizer.minimize(cost)
return cost
def reward_func(samples, sample_length):
"""toy reward"""
def discount_reward(reward, sequence_length, discount=1.0):
return discount_reward_1d(reward, sequence_length, discount)
def discount_reward_1d(reward, sequence_length, discount=1.0, dtype=None):
if sequence_length is None:
raise ValueError(
'sequence_length must not be `None` for 1D reward.'
)
reward = np.array(reward)
sequence_length = np.array(sequence_length)
batch_size = reward.shape[0]
max_seq_length = np.max(sequence_length)
dtype = dtype or reward.dtype
if discount == 1.0:
dmat = np.ones([batch_size, max_seq_length], dtype=dtype)
else:
steps = np.tile(np.arange(max_seq_length), [batch_size, 1])
mask = np.asarray(
steps < (sequence_length - 1)[:, None], dtype=dtype
)
# Make each row = [discount, ..., discount, 1, ..., 1]
dmat = mask * discount + (1 - mask)
dmat = np.cumprod(dmat[:, ::-1], axis=1)[:, ::-1]
disc_reward = dmat * reward[:, None]
disc_reward = mask_sequences(disc_reward, sequence_length, dtype=dtype)
return disc_reward
def mask_sequences(sequence, sequence_length, dtype=None, time_major=False):
sequence = np.array(sequence)
sequence_length = np.array(sequence_length)
rank = sequence.ndim
if rank < 2:
raise ValueError("`sequence` must be 2D or higher order.")
batch_size = sequence.shape[0]
max_time = sequence.shape[1]
dtype = dtype or sequence.dtype
if time_major:
sequence = np.transpose(sequence, axes=[1, 0, 2])
steps = np.tile(np.arange(max_time), [batch_size, 1])
mask = np.asarray(steps < sequence_length[:, None], dtype=dtype)
for _ in range(2, rank):
mask = np.expand_dims(mask, -1)
sequence = sequence * mask
if time_major:
sequence = np.transpose(sequence, axes=[1, 0, 2])
return sequence
samples = np.array(samples)
sample_length = np.array(sample_length)
# length reward
reward = (5 - np.abs(sample_length - 5)).astype("float32")
# repeat punishment to trapped into local minima getting all same words
# beam search to get more than one sample may also can avoid this
for i in range(reward.shape[0]):
reward[i] += (
-10
if sample_length[i] > 1
and np.all(samples[i][: sample_length[i] - 1] == samples[i][0])
else 0
)
return discount_reward(reward, sample_length, discount=1.0).astype(
"float32"
)
class MLE:
"""teacher-forcing MLE training"""
def __init__(self, lr=None):
self.lr = lr
def learn(self, probs, label, weight=None, length=None):
loss = paddle.nn.functional.cross_entropy(
input=probs,
label=label,
soft_label=False,
reduction='none',
use_softmax=False,
)
max_seq_len = paddle.shape(probs)[1]
mask = paddle.static.nn.sequence_lod.sequence_mask(
length, maxlen=max_seq_len, dtype="float32"
)
loss = loss * mask
loss = paddle.mean(loss, axis=[0])
loss = paddle.sum(loss)
optimizer = paddle.optimizer.Adam(self.lr)
optimizer.minimize(loss)
return loss
class SeqPGAgent:
def __init__(
self,
model_cls,
alg_cls=PolicyGradient,
model_hparams={},
alg_hparams={},
executor=None,
main_program=None,
startup_program=None,
seed=None,
):
self.main_program = (
base.Program() if main_program is None else main_program
)
self.startup_program = (
base.Program() if startup_program is None else startup_program
)
if seed is not None:
paddle.seed(seed)
self.build_program(model_cls, alg_cls, model_hparams, alg_hparams)
self.executor = executor
def build_program(self, model_cls, alg_cls, model_hparams, alg_hparams):
with base.program_guard(self.main_program, self.startup_program):
source = paddle.static.data(
name="src", shape=[None, None], dtype="int64"
)
source_length = paddle.static.data(
name="src_sequence_length", shape=[None], dtype="int64"
)
# only for teacher-forcing MLE training
target = paddle.static.data(
name="trg", shape=[None, None], dtype="int64"
)
target_length = paddle.static.data(
name="trg_sequence_length", shape=[None], dtype="int64"
)
label = paddle.static.data(
name="label", shape=[None, None, 1], dtype="int64"
)
self.model = model_cls(**model_hparams)
self.alg = alg_cls(**alg_hparams)
self.probs, self.samples, self.sample_length = self.model(
source, source_length, target, target_length
)
self.samples.stop_gradient = True
self.reward = paddle.static.data(
name="reward",
shape=[None, None], # batch_size, seq_len
dtype=self.probs.dtype,
)
self.samples.stop_gradient = False
self.cost = self.alg.learn(
self.probs, self.samples, self.reward, self.sample_length
)
# to define the same parameters between different programs
self.pred_program = self.main_program._prune_with_input(
[source.name, source_length.name],
[self.probs, self.samples, self.sample_length],
)
def predict(self, feed_dict):
samples, sample_length = self.executor.run(
self.pred_program,
feed=feed_dict,
fetch_list=[self.samples, self.sample_length],
)
return samples, sample_length
def learn(self, feed_dict, fetch_list):
results = self.executor.run(
self.main_program, feed=feed_dict, fetch_list=fetch_list
)
return results
class ModuleApiTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls._np_rand_state = np.random.get_state()
cls._py_rand_state = random.getstate()
cls._random_seed = 123
np.random.seed(cls._random_seed)
random.seed(cls._random_seed)
cls.model_cls = type(
cls.__name__ + "Model",
(Layer,),
{
"__init__": cls.model_init_wrapper(cls.model_init),
"forward": cls.model_forward,
},
)
@classmethod
def tearDownClass(cls):
np.random.set_state(cls._np_rand_state)
random.setstate(cls._py_rand_state)
@staticmethod
def model_init_wrapper(func):
def __impl__(self, *args, **kwargs):
Layer.__init__(self)
func(self, *args, **kwargs)
return __impl__
@staticmethod
def model_init(model, *args, **kwargs):
raise NotImplementedError(
"model_init acts as `Model.__init__`, thus must implement it"
)
@staticmethod
def model_forward(model, *args, **kwargs):
return model.module(*args, **kwargs)
def make_inputs(self):
# TODO(guosheng): add default from `self.inputs`
raise NotImplementedError(
"model_inputs makes inputs for model, thus must implement it"
)
def setUp(self):
"""
For the model which wraps the module to be tested:
Set input data by `self.inputs` list
Set init argument values by `self.attrs` list/dict
Set model parameter values by `self.param_states` dict
Set expected output data by `self.outputs` list
We can create a model instance and run once with these.
"""
self.inputs = []
self.attrs = {}
self.param_states = {}
self.outputs = []
def _calc_output(self, place, mode="test", dygraph=True):
if dygraph:
base.enable_dygraph(place)
else:
base.disable_dygraph()
gen = paddle.seed(self._random_seed)
if paddle.framework.use_pir_api():
with paddle.pir_utils.OldIrGuard():
paddle.framework.random._manual_program_seed(self._random_seed)
paddle.framework.random._manual_program_seed(self._random_seed)
else:
paddle.framework.random._manual_program_seed(self._random_seed)
scope = base.core.Scope()
with base.scope_guard(scope):
layer = (
self.model_cls(**self.attrs)
if isinstance(self.attrs, dict)
else self.model_cls(*self.attrs)
)
model = Model(layer, inputs=self.make_inputs())
model.prepare()
if self.param_states:
model.load(self.param_states, optim_state=None)
return model.predict_batch(self.inputs)
def check_output_with_place(self, place, mode="test"):
dygraph_output = self._calc_output(place, mode, dygraph=True)
stgraph_output = self._calc_output(place, mode, dygraph=False)
expect_output = getattr(self, "outputs", None)
for actual_t, expect_t in zip(dygraph_output, stgraph_output):
np.testing.assert_allclose(actual_t, expect_t, rtol=1e-05, atol=0)
if expect_output:
for actual_t, expect_t in zip(dygraph_output, expect_output):
np.testing.assert_allclose(
actual_t, expect_t, rtol=1e-05, atol=0
)
def check_output(self):
devices = ["CPU"]
if base.is_compiled_with_cuda() or is_custom_device():
devices.append(get_device_place())
for device in devices:
place = set_device(device)
self.check_output_with_place(place)
class TestBeamSearch(ModuleApiTest):
def setUp(self):
paddle.set_default_dtype("float64")
shape = (8, 32)
self.inputs = [
np.random.random(shape).astype("float64"),
np.random.random(shape).astype("float64"),
]
self.outputs = None
self.attrs = {
"vocab_size": 100,
"embed_dim": 32,
"hidden_size": 32,
}
self.param_states = {}
@staticmethod
def model_init(
self,
vocab_size,
embed_dim,
hidden_size,
bos_id=0,
eos_id=1,
beam_size=4,
max_step_num=20,
):
self.embedder = Embedding(vocab_size, embed_dim)
self.output_layer = nn.Linear(hidden_size, vocab_size)
self.cell = nn.LSTMCell(embed_dim, hidden_size)
self.max_step_num = max_step_num
self.beam_search_decoder = BeamSearchDecoder(
self.cell,
start_token=bos_id,
end_token=eos_id,
beam_size=beam_size,
embedding_fn=self.embedder,
output_fn=self.output_layer,
)
@staticmethod
def model_forward(model, init_hidden, init_cell):
return dynamic_decode(
model.beam_search_decoder,
[init_hidden, init_cell],
max_step_num=model.max_step_num,
impute_finished=True,
is_test=True,
)[0]
def make_inputs(self):
inputs = [
Input([None, self.inputs[0].shape[-1]], "float64", "init_hidden"),
Input([None, self.inputs[1].shape[-1]], "float64", "init_cell"),
]
return inputs
def test_check_output(self):
self.setUp()
self.make_inputs()
self.check_output()
class EncoderCell(SimpleRNNCell):
def __init__(
self,
num_layers,
input_size,
hidden_size,
dropout_prob=0.0,
init_scale=0.1,
):
super().__init__(input_size, hidden_size)
self.dropout_prob = dropout_prob
# use add_sublayer to add multi-layers
self.lstm_cells = []
for i in range(num_layers):
self.lstm_cells.append(
self.add_sublayer(
f"lstm_{i}",
LSTMCell(
input_size=input_size if i == 0 else hidden_size,
hidden_size=hidden_size,
),
)
)
def forward(self, step_input, states):
new_states = []
for i, lstm_cell in enumerate(self.lstm_cells):
out, new_state = lstm_cell(step_input, states[i])
step_input = (
layers.dropout(
out,
self.dropout_prob,
dropout_implementation='upscale_in_train',
)
if self.dropout_prob > 0
else out
)
new_states.append(new_state)
return step_input, new_states
@property
def state_shape(self):
return [cell.state_shape for cell in self.lstm_cells]
class Encoder(Layer):
def __init__(
self,
vocab_size,
embed_dim,
hidden_size,
num_layers,
dropout_prob=0.0,
init_scale=0.1,
):
super().__init__()
self.embedder = Embedding(vocab_size, embed_dim)
self.stack_lstm = RNN(
EncoderCell(
num_layers, embed_dim, hidden_size, dropout_prob, init_scale
),
is_reverse=False,
time_major=False,
)
def forward(self, sequence, sequence_length):
inputs = self.embedder(sequence)
encoder_output, encoder_state = self.stack_lstm(
inputs, sequence_length=sequence_length
)
return encoder_output, encoder_state
DecoderCell = EncoderCell
class Decoder(Layer):
def __init__(
self,
vocab_size,
embed_dim,
hidden_size,
num_layers,
dropout_prob=0.0,
init_scale=0.1,
):
super().__init__()
self.embedder = Embedding(vocab_size, embed_dim)
self.stack_lstm = RNN(
DecoderCell(
num_layers, embed_dim, hidden_size, dropout_prob, init_scale
),
is_reverse=False,
time_major=False,
)
self.output_layer = Linear(hidden_size, vocab_size, bias_attr=False)
def forward(self, target, decoder_initial_states):
inputs = self.embedder(target)
decoder_output, _ = self.stack_lstm(
inputs, initial_states=decoder_initial_states
)
predict = self.output_layer(decoder_output)
return predict
class TrainingHelper:
def __init__(self, inputs, sequence_length, time_major=False):
self.inputs = inputs
self.sequence_length = sequence_length
self.time_major = time_major
self.inputs_ = paddle.utils.map_structure(
lambda x: paddle.nn.functional.pad(
x,
pad=(
([0, 1] + [0, 0] * (len(x.shape) - 1))
if time_major
else ([0, 0, 0, 1] + [0, 0] * (len(x.shape) - 2))
),
),
self.inputs,
)
def initialize(self):
init_finished = paddle.equal(
self.sequence_length,
paddle.full(
shape=[1], dtype=self.sequence_length.dtype, fill_value=0
),
)
init_inputs = paddle.utils.map_structure(
lambda x: x[0] if self.time_major else x[:, 0], self.inputs
)
return init_inputs, init_finished
def sample(self, time, outputs, states):
sample_ids = paddle.argmax(outputs, axis=-1)
return sample_ids
def next_inputs(self, time, outputs, states, sample_ids):
time = (
paddle.cast(time, "int32")
if convert_dtype(time.dtype) not in ["int32"]
else time
)
if self.sequence_length.dtype != time.dtype:
self.sequence_length = paddle.cast(self.sequence_length, time.dtype)
next_time = time + 1
finished = paddle.less_equal(self.sequence_length, next_time)
def _slice(x):
axes = [0 if self.time_major else 1]
return paddle.squeeze(
paddle.slice(
x, axes=axes, starts=[next_time], ends=[next_time + 1]
),
axis=axes,
)
next_inputs = paddle.utils.map_structure(_slice, self.inputs_)
return finished, next_inputs, states
class BasicDecoder(paddle.nn.decode.Decoder):
def __init__(self, cell, helper, output_fn=None):
super().__init__()
self.cell = cell
self.helper = helper
self.output_fn = output_fn
def initialize(self, initial_cell_states):
(initial_inputs, initial_finished) = self.helper.initialize()
return initial_inputs, initial_cell_states, initial_finished
class OutputWrapper(
collections.namedtuple("OutputWrapper", ("cell_outputs", "sample_ids"))
):
pass
def step(self, time, inputs, states, **kwargs):
cell_outputs, cell_states = self.cell(inputs, states, **kwargs)
if self.output_fn is not None:
cell_outputs = self.output_fn(cell_outputs)
sample_ids = self.helper.sample(
time=time, outputs=cell_outputs, states=cell_states
)
sample_ids.stop_gradient = True
(finished, next_inputs, next_states) = self.helper.next_inputs(
time=time,
outputs=cell_outputs,
states=cell_states,
sample_ids=sample_ids,
)
outputs = self.OutputWrapper(cell_outputs, sample_ids)
return (outputs, next_states, next_inputs, finished)
class BaseModel(Layer):
def __init__(
self,
vocab_size=10,
embed_dim=32,
hidden_size=32,
num_layers=1,
dropout_prob=0.0,
init_scale=0.1,
):
super().__init__()
self.hidden_size = hidden_size
self.word_embedding = Embedding(vocab_size, embed_dim)
self.encoder = Encoder(
vocab_size,
embed_dim,
hidden_size,
num_layers,
dropout_prob,
init_scale,
)
self.decoder = Decoder(
vocab_size,
embed_dim,
hidden_size,
num_layers,
dropout_prob,
init_scale,
)
def forward(self, src, src_length, trg, trg_length):
encoder_output = self.encoder(src, src_length)
trg_emb = self.decoder.embedder(trg)
helper = TrainingHelper(inputs=trg_emb, sequence_length=trg_length)
decoder = BasicDecoder(self.decoder.stack_lstm.cell, helper)
(
decoder_output,
decoder_final_state,
dec_seq_lengths,
) = dynamic_decode(
decoder,
inits=self.decoder.stack_lstm.cell.get_initial_states(
encoder_output
),
impute_finished=True,
is_test=False,
return_length=True,
)
logits, samples, sample_length = (
decoder_output.cell_outputs,
decoder_output.sample_ids,
dec_seq_lengths,
)
return logits
class TestDynamicDecode(ModuleApiTest):
def setUp(self):
paddle.set_default_dtype("float64")
shape = (1, 10)
bs_shape = 1
self.inputs = [
np.random.randint(0, 10, size=shape).astype("int64"),
np.random.randint(0, 10, size=bs_shape).astype("int64"),
np.random.randint(0, 10, size=shape).astype("int64"),
np.random.randint(0, 10, size=bs_shape).astype("int64"),
]
self.outputs = None
self.attrs = {
"vocab_size": 10,
"embed_dim": 32,
"hidden_size": 32,
}
self.param_states = {}
@staticmethod
def model_init(
self,
vocab_size,
embed_dim,
hidden_size,
bos_id=0,
eos_id=1,
):
self.model = BaseModel(
vocab_size=vocab_size, embed_dim=embed_dim, hidden_size=hidden_size
)
@staticmethod
def model_forward(model, src, src_length, trg, trg_length):
return model.model(src, src_length, trg, trg_length)
def make_inputs(self):
inputs = [
Input([None, None], "int64", "src"),
Input([None], "int64", "src_length"),
Input([None, None], "int64", "trg"),
Input([None], "int64", "trg_length"),
]
return inputs
def test_check_output(self):
self.setUp()
self.make_inputs()
self.check_output()
if __name__ == '__main__':
unittest.main()