Masking دستی و Ragged Tensor
Masking خودکار برای Modelهای ساده عالی است، اما وقتی Conv1D، چند Branch یا Layerهای Custom با Recurrent Layerها ترکیب شوند، گاهی باید Mask را دستی محاسبه کرد. در Functional API میتوان IDهای غیرصفر را بهعنوان Mask ساخت و مستقیماً به GRU یا LSTM داد:
inputs = tf.keras.layers.Input(shape=[], dtype=tf.string)
token_ids = text_vec_layer(inputs)
mask = tf.math.not_equal(token_ids, 0)
Z = tf.keras.layers.Embedding(vocab_size, embed_size)(token_ids)
Z = tf.keras.layers.GRU(128, dropout=0.2)(Z, mask=mask)
outputs = tf.keras.layers.Dense(1, activation="sigmoid")(Z)
model = tf.keras.Model(inputs=[inputs], outputs=[outputs])
راه دیگر استفاده از Ragged Tensor است. با ragged=True در TextVectorization، Sequenceهای با طول متفاوت بدون Padding در یک RaggedTensor نگهداری میشوند. Recurrent Layerهای Keras از RaggedTensor پشتیبانی میکنند، بنابراین نیازی به mask_zero=True نیست.
استفادهٔ مجدد از Embedding و Language Modelهای Pretrained
Embedding آموختهشده از فقط ۲۵هزار Review میتواند مفید باشد، اما Corpusهای عظیم Representationهای قویتری تولید میکنند. Word2vec، GloVe و FastText نمونههایی از Word Embeddingهای Pretrained هستند. محدودیت مهم آنها این است که یک Word همیشه یک Vector ثابت دارد؛ مثلاً معنای «right» در «left and right» و «right and wrong» متفاوت است، اما Embedding کلاسیک تفاوت Context را نمیبیند.
مدل ELMo این محدودیت را با Contextual Embedding حل کرد: Representation یک Word از Hidden Stateهای یک Language Model دوطرفه به دست میآید و بنابراین به جمله وابسته است. اندکی بعد ULMFiT نشان داد Pretraining بدون Label روی Corpus عظیم و سپس Fine-tuning روی Task خاص میتواند با Label بسیار کمتر به Accuracy خوب برسد. این نقطه شروع دورهای شد که Reuse کردن Language Modelهای Pretrained در NLP به استاندارد تبدیل شد.
Universal Sentence Encoder از TensorFlow Hub
import os
import tensorflow_hub as hub
os.environ["TFHUB_CACHE_DIR"] = "my_tfhub_cache"
model = tf.keras.Sequential([
hub.KerasLayer(
"https://tfhub.dev/google/universal-sentence-encoder/4",
trainable=True, dtype=tf.string, input_shape=[]),
tf.keras.layers.Dense(64, activation="relu"),
tf.keras.layers.Dense(1, activation="sigmoid")
])
model.compile(loss="binary_crossentropy", optimizer="nadam",
metrics=["accuracy"])
با trainable=True خود Universal Sentence Encoder نیز Fine-tune میشود. Model نزدیک 1GB است و بهتر است Cache Directory مشخص شود تا هر بار Download نشود. Accuracy Validation این مثال از 90% عبور میکند.
Encoder-Decoder برای Neural Machine Translation
اکنون یک Model برای ترجمهٔ English به Spanish میسازیم. English Sentence وارد Encoder میشود و Decoder ترجمهٔ Spanish را تولید میکند. هنگام Training، Target اسپانیایی یک Step Shift میشود و Word صحیح قبلی به Decoder داده میشود. این روش Teacher Forcing نام دارد و Training را بسیار سریعتر و پایدارتر میکند.
Decoder با Token ویژهٔ <sos> شروع میکند و باید جمله را با <eos> تمام کند. در هر Step، Dense+Softmax یک Probability برای هر Word در Vocabulary مقصد میسازد.
شکل 16-3. Encoder-Decoder برای Neural Machine Translation
در Inference دیگر Target واقعی در اختیار Decoder نیست؛ بنابراین Word تولیدشده در Step قبلی بهعنوان Input Step بعد استفاده میشود.
شکل 16-4. Encoder-Decoder برای Neural Machine Translation
آمادهسازی Dataset دوزبانه
Dataset شامل جفت Sentenceهای English/Spanish است. برای هر زبان TextVectorization جدا ساخته و روی Text همان زبان Adapt میشود. Vocabulary مقصد شامل Tokenهای SOS و EOS نیز هست. سپس Dataset به Inputهای Encoder، Inputهای Decoder و Targetهای Decoder تقسیم میشود.
text_vec_layer_en = tf.keras.layers.TextVectorization(
max_tokens=vocab_size, output_sequence_length=max_length)
text_vec_layer_es = tf.keras.layers.TextVectorization(
max_tokens=vocab_size, output_sequence_length=max_length,
standardize=strip_chars)
در عمل Sentenceهای بسیار بلند میتوانند Truncate شوند و Padding برای Batch شدن استفاده میشود.
ساخت Encoder و Decoder با Functional API
Model دو Input دارد؛ یکی برای Sentence English و دیگری برای Spanish Shiftشده. Word IDها Embedding میشوند. Encoder یک LSTM است که State کوتاهمدت و بلندمدت نهایی را تولید میکند و این Stateها به Decoder بهعنوان Initial State داده میشوند:
encoder_inputs = tf.keras.layers.Input(shape=[], dtype=tf.string)
decoder_inputs = tf.keras.layers.Input(shape=[], dtype=tf.string)
encoder_input_ids = text_vec_layer_en(encoder_inputs)
decoder_input_ids = text_vec_layer_es(decoder_inputs)
embed_size = 128
encoder_embedding_layer = tf.keras.layers.Embedding(vocab_size, embed_size,
mask_zero=True)
decoder_embedding_layer = tf.keras.layers.Embedding(vocab_size, embed_size,
mask_zero=True)
encoder_embeddings = encoder_embedding_layer(encoder_input_ids)
decoder_embeddings = decoder_embedding_layer(decoder_input_ids)
encoder = tf.keras.layers.LSTM(512, return_state=True)
encoder_outputs, *encoder_state = encoder(encoder_embeddings)
decoder = tf.keras.layers.LSTM(512, return_sequences=True)
decoder_outputs = decoder(decoder_embeddings,
initial_state=encoder_state)
output_layer = tf.keras.layers.Dense(vocab_size, activation="softmax")
Y_proba = output_layer(decoder_outputs)
model = tf.keras.Model(inputs=[encoder_inputs, decoder_inputs], outputs=[Y_proba])
Loss همان Sparse Categorical Cross-Entropy است. در Inference یک Function ترجمه، Decoder Input را از SOS شروع میکند، Model را اجرا میکند، Word بعدی را برمیگزیند و آن را به Sentence جزئی اضافه میکند تا EOS تولید شود.
Bidirectional RNN
Encoder معمولی در هر Time Step فقط Past را دیده است. اما برای Encode کردن یک Sentence کامل، Future نیز از قبل موجود است. بنابراین Encoder میتواند یک RNN Forward و یک RNN Backward داشته باشد و Output آنها را ترکیب کند.
شکل 16-5. Bidirectional RNN
در Keras با Wrapper زیر ساخته میشود:
encoder = tf.keras.layers.Bidirectional(
tf.keras.layers.LSTM(256, return_state=True))
چون هر Direction در LSTM دو State دارد، Wrapper چهار State برمیگرداند. میتوان Short-term Stateهای Forward/Backward را Concatenate کرد و همین کار را برای Long-term Stateها انجام داد تا Decoder با State 512بعدی Initialize شود.
Beam Search
Greedy Decoding در هر Step فقط محتملترین Word را نگه میدارد، اما انتخاب محلی ممکن است به Translation ضعیف منجر شود. Beam Search چند Candidate برتر را همزمان دنبال میکند. اگر Beam Width برابر 3 باشد، در Step اول سه Word برتر نگه داشته میشوند؛ در Step بعد هر Candidate با هزاران Word ممکن توسعه مییابد و از میان همهٔ Sequenceهای جدید فقط سه Sequence با Probability تجمعی بهتر باقی میمانند.
شکل 16-6. Beam Search
Beam Search بدون Retrain کردن Model میتواند Translation را بهطور محسوسی بهتر کند، ولی Cost محاسباتی متناسب با Beam Width افزایش مییابد.
چرا Encoder-Decoder ساده روی Sentenceهای طولانی ضعیف میشود؟
در معماری ساده، تمام اطلاعات Sentence ورودی باید در Final State فشرده شود و Decoder فقط همین Vector را بهعنوان Summary دریافت میکند. برای Sentence کوتاه کافی است، اما در Sentence بلند اطلاعات اولیه بهتدریج از State محو میشوند. راهحل مهم این مشکل Attention است: Decoder بهجای اتکا فقط به Final State، در هر Step میتواند مستقیماً به همهٔ Outputهای Encoder نگاه کند و بخش مرتبط را وزن بیشتری بدهد. بخش بعد این Mechanism را دقیق بررسی میکند.