فصل ۱۲: مدلها و آموزش سفارشی با TensorFlow
تا اینجا تقریباً فقط از API سطح بالای TensorFlow یعنی Keras استفاده کردیم و با همان ابزارها مسیر زیادی پیمودیم: شبکههای رگرسیون و طبقهبندی، معماریهای Wide & Deep، شبکههای self-normalizing، Batch Normalization، Dropout و scheduleهای نرخ یادگیری را ساختیم. در عمل حدود ۹۵٪ کاربردها با Keras و tf.data قابل انجاماند. اما گاهی برای loss، metric، layer، model، initializer، regularizer، weight constraint یا حتی خود training loop به کنترل بیشتری نیاز داریم؛ مثلاً بخواهیم روی گرادیانها تبدیلی خاص انجام دهیم یا قسمتهای مختلف شبکه را با optimizerهای متفاوت آموزش دهیم. این فصل به API سطح پایین Python در TensorFlow میپردازد و نشان میدهد چگونه مدلها و الگوریتمهای آموزشی سفارشی را با قابلیت ساخت خودکار graph سریعتر کنیم.
مروری سریع بر TensorFlow
TensorFlow کتابخانهای قدرتمند برای محاسبات عددی است که بهطور ویژه برای machine learning در مقیاس بزرگ بهینه شده، هرچند کاربرد آن محدود به ML نیست. این کتابخانه توسط تیم Google Brain توسعه یافت، در سرویسهای بزرگ گوگل به کار رفت و از نوامبر ۲۰۱۵ متنباز شد. امروزه در پروژههای گوناگون از طبقهبندی تصویر و NLP تا سیستم توصیهگر و forecasting سری زمانی استفاده میشود.
قابلیتهای اصلی TensorFlow را میتوان چنین خلاصه کرد:
- هسته آن از نظر کار با آرایهها بسیار شبیه NumPy است، با پشتیبانی مستقیم از GPU.
- محاسبات توزیعشده روی چند device و چند server را پشتیبانی میکند.
- نوعی کامپایلر JIT دارد که graph محاسباتی را از تابع Python استخراج، بهینه و اجرا میکند؛ مثلاً nodeهای بیاستفاده را حذف و عملیات مستقل را موازی اجرا میکند.
- graphهای محاسباتی را میتوان به فرم portable صادر کرد؛ بنابراین مدل میتواند مثلاً با Python روی Linux آموزش ببیند و با Java روی Android اجرا شود.
- reverse-mode autodiff را پیادهسازی میکند و optimizerهایی مانند RMSProp و Nadam دارد؛ در نتیجه کمینهکردن انواع loss functionها آسان است.
روی این هسته قابلیتهای فراوان دیگری ساخته شدهاند: Keras، ابزارهای بارگذاری و پیشپردازش مانند tf.data و tf.io، عملیات تصویر با tf.image، پردازش سیگنال با tf.signal و بستههای دیگر. API TensorFlow بسیار بزرگ است و مرور مستندات رسمی بخش مهمی از کار عملی است.
شکل ۱۲-۱ - نمای کلی لایههای مختلف Python API در TensorFlow.
در پایینترین سطح، هر operation یا op با C++ بسیار بهینه پیادهسازی میشود. بسیاری از opها چند پیادهسازی به نام kernel دارند؛ هر kernel برای device مشخصی مانند CPU، GPU یا TPU طراحی شده است. GPU محاسبه را به بخشهای بسیار کوچک تقسیم و روی threadهای فراوان موازی اجرا میکند. TPU نیز ASIC تخصصی برای عملیات deep learning است.
بیشتر برنامهها از APIهای سطح بالا، بهویژه Keras و tf.data، استفاده میکنند؛ وقتی انعطاف بیشتر لازم باشد، API سطح پایین و tensorها مستقیماً وارد کار میشوند. در هر دو حالت execution engine عملیات را بهصورت کارآمد و در صورت پیکربندی روی چند device و حتی چند ماشین اجرا میکند.
شکل ۱۲-۲ - معماری TensorFlow و ارتباط APIهای سطح بالا، هسته اجرای عملیات و deviceها.
TensorFlow روی Windows، Linux و macOS اجرا میشود و با TensorFlow Lite روی iOS و Android نیز قابل استقرار است. APIهایی برای C++، Java و Swift وجود دارد و TensorFlow.js اجازه میدهد مدل مستقیماً در مرورگر اجرا شود.
اکوسیستم TensorFlow فراتر از خود کتابخانه است: TensorBoard برای visualization، TensorFlow Extended یا TFX برای production شامل data validation، preprocessing، model analysis و serving، TensorFlow Hub برای دانلود و reuse شبکههای pretrained، و model garden برای معماریهای آماده. افزون بر این، پروژههای متنباز فراوان و پیادهسازی مقالههای پژوهشی در دسترساند.
استفاده از TensorFlow مانند NumPy
هسته API TensorFlow حول tensorها میچرخد؛ داده از operationی به operation دیگر «جریان» پیدا میکند و همین منشأ نام TensorFlow است. Tensor شبیه numpy.ndarray است: معمولاً آرایهای چندبعدی، اما میتواند scalar سادهای مانند ۴۲ نیز باشد. Tensorها پایه ساخت loss، metric، layer و مدل سفارشیاند.
Tensorها و Operationها
با tf.constant() میتوان یک tensor ساخت:
import tensorflow as tf
t = tf.constant([[1., 2., 3.], [4., 5., 6.]])
print(t)
# <tf.Tensor: shape=(2, 3), dtype=float32, numpy=
# array([[1., 2., 3.],
# [4., 5., 6.]], dtype=float32)>
مانند ndarray، یک tf.Tensor شکل و dtype دارد:
t.shape
# TensorShape([2, 3])
t.dtype
# tf.float32
Indexing نیز بسیار شبیه NumPy است:
t[:, 1:]
# [[2., 3.],
# [5., 6.]]
t[..., 1, tf.newaxis]
# [[2.],
# [5.]]
عملیات ریاضی متنوعی مستقیماً روی tensorها وجود دارد:
t + 10
# [[11., 12., 13.],
# [14., 15., 16.]]
tf.square(t)
# [[ 1., 4., 9.],
# [16., 25., 36.]]
t @ tf.transpose(t)
# [[14., 32.],
# [32., 77.]]
عبارت t + 10 معادل tf.add(t, 10) است؛ Python متد magic مربوط را صدا میزند و TensorFlow عملیات را انجام میدهد. عملگرهای - و * نیز پشتیبانی میشوند. عملگر @ برای ضرب ماتریسی است و معادل tf.matmul() عمل میکند.
بسیاری از تابعها alias دارند؛ مثلاً tf.add() و tf.math.add() یک تابعاند. این کار هم نام کوتاه برای operationهای پرکاربرد میدهد و هم سازماندهی packageها را حفظ میکند.
Tensor میتواند scalar باشد و در این حالت shape خالی است:
tf.constant(42)
# <tf.Tensor: shape=(), dtype=int32, numpy=42>
Keras نیز API سطح پایینی در tf.keras.backend دارد که معمولاً با K وارد میشد. زمانی که Keras چند backend داشت، تابعهایی مانند K.square() برای portable بودن کد مفید بودند. در نسخه TensorFlow-only، بهتر است مستقیماً از API سطح پایین TensorFlow، مثلاً tf.square()، استفاده شود؛ برخی نامهای قدیمی فقط برای backward compatibility باقی ماندهاند.
تقریباً همه عملیات ریاضی پایه وجود دارند: tf.add()، tf.multiply()، tf.square()، tf.exp()، tf.sqrt() و نیز operationهایی مشابه NumPy مثل tf.reshape()، tf.squeeze() و tf.tile(). بعضی نامها متفاوتاند: tf.reduce_mean()، tf.reduce_sum()، tf.reduce_max() و tf.math.log() تقریباً معادل np.mean()، np.sum()، np.max() و np.log() هستند.
تفاوت نامها گاهی منعکسکننده تفاوت واقعی رفتار است. در TensorFlow برای transpose باید tf.transpose(t) را نوشت؛ برخلاف t.T در NumPy که فقط یک view transposed از همان داده است، TensorFlow tensor جدیدی با کپی transposed میسازد. همچنین tf.reduce_sum() روی GPU ممکن است بهعلت ترتیب متفاوت جمع اعداد float32 در اجراهای مختلف تفاوت بسیار ناچیزی ایجاد کند؛ محدودیت precision شناور باعث میشود جمع در ترتیبهای متفاوت دقیقاً بیتبهبیت یکسان نباشد. tf.reduce_mean() نیز همین ملاحظه را دارد، در حالی که tf.reduce_max() deterministic است.
Tensor و NumPy
TensorFlow و NumPy بهخوبی با هم کار میکنند: میتوان از آرایه NumPy tensor ساخت، tensor را به NumPy تبدیل کرد و operationهای هرکدام را روی داده دیگری بهکار برد:
import numpy as np
a = np.array([2., 4., 5.])
tf.constant(a)
# <tf.Tensor: shape=(3,), dtype=float64, numpy=array([2., 4., 5.])>
t.numpy() # or np.array(t)
# array([[1., 2., 3.],
# [4., 5., 6.]], dtype=float32)
tf.square(a)
# array-like tensor: [4., 16., 25.]
np.square(t)
# array([[ 1., 4., 9.],
# [16., 25., 36.]], dtype=float32)
تبدیل نوع داده
تبدیل خودکار dtype میتواند هزینه performance داشته باشد و بهسادگی پنهان بماند. برای جلوگیری از این اتفاق، TensorFlow بسیاری از type conversionها را خودکار انجام نمیدهد و اگر دو tensor نوع ناسازگار داشته باشند خطا میدهد. مثلاً float و int یا حتی float32 و float64 را نمیتوان بدون تبدیل صریح با هم جمع کرد:
tf.constant(2.) + tf.constant(40)
# InvalidArgumentError ...
tf.constant(2.) + tf.constant(40., dtype=tf.float64)
# InvalidArgumentError ...
وقتی تبدیل واقعاً لازم است، از tf.cast() استفاده میکنیم:
t2 = tf.constant(40., dtype=tf.float64)
tf.constant(2.0) + tf.cast(t2, tf.float32)
# <tf.Tensor: shape=(), dtype=float32, numpy=42.0>
Variableها
tf.Tensorهایی که تا اینجا دیدیم immutable هستند و نمیتوان مقدارشان را درجا تغییر داد. برای وزن شبکه عصبی این کافی نیست، چون backpropagation باید وزنها را مرتب بهروزرسانی کند و optimizerهایی مانند momentum نیز state متغیر نگه میدارند. برای این منظور tf.Variable داریم:
v = tf.Variable([[1., 2., 3.], [4., 5., 6.]])
print(v)
# <tf.Variable 'Variable:0' shape=(2, 3) dtype=float32 ...>
Variable در بیشتر operationها مانند Tensor رفتار میکند و با NumPy نیز سازگار است، اما با assign()، assign_add() و assign_sub() میتوان آن را درجا تغییر داد. slice یا cellها نیز متد assign() دارند و برای update پراکنده میتوان از روشهای scatter استفاده کرد:
v.assign(2 * v) # [[2., 4., 6.], [8., 10., 12.]]
v[0, 1].assign(42) # [[2., 42., 6.], [8., 10., 12.]]
v[:, 2].assign([0., 1.]) # [[2., 42., 0.], [8., 10., 1.]]
v.scatter_nd_update(
indices=[[0, 0], [1, 2]],
updates=[100., 200.])
اما assignment مستقیم Python کار نمیکند:
v[1] = [7., 8., 9.]
# TypeError: 'ResourceVariable' object does not support item assignment
در عمل کمتر لازم است Variable را دستی بسازیم؛ Keras متد add_weight() دارد و optimizerها معمولاً پارامترهای مدل را خودکار update میکنند.
ساختارهای داده دیگر در TensorFlow
tf.SparseTensor
- برای نمایش کارآمد tensorهایی که بیشتر عناصرشان صفر است. operationهای مرتبط در package
tf.sparse قرار دارند.
tf.TensorArray
- لیستی از tensorهاست. طول آن بهطور پیشفرض ثابت است اما میتواند قابل توسعه باشد. همه tensorهای داخل آن باید shape و dtype یکسان داشته باشند.
tf.RaggedTensor
- لیستهایی از tensorهای همرتبه و همنوع با اندازههای متفاوت را نمایش میدهد. ابعادی که طولشان متغیر است ragged dimensions نام دارند و operationهای مربوط در
tf.ragged هستند.
- String Tensor
tf.string رشته بایتی را نگه میدارد، نه رشته Unicode به معنای مستقیم. رشته Python 3 مانند "café" به UTF-8 encode میشود. برای نمایش code pointهای Unicode میتوان tensor از نوع tf.int32 داشت. package tf.strings operationهای تبدیل و پردازش رشته را فراهم میکند. یک مقدار tf.string از دید shape اتمیک است و طول رشته در shape ظاهر نمیشود؛ پس از تبدیل به tensor کدپوینتها، طول در shape دیده میشود.
- Setها
- با tensor معمولی یا sparse نمایش داده میشوند؛ مثلاً
tf.constant([[1, 2], [3, 4]]) دو مجموعه {1,2} و {3,4} را نمایندگی میکند. operationها در tf.sets قرار دارند.
- Queueها
- برای نگهداری tensorها بین چند step استفاده میشوند. TensorFlow انواع
FIFOQueue، PriorityQueue، RandomShuffleQueue و PaddingFIFOQueue را در tf.queue فراهم میکند.
با tensorها، operationها، Variableها و این ساختارهای داده اکنون ابزارهای بنیادی لازم برای ساخت مدلها و الگوریتمهای آموزشی سفارشی را در اختیار داریم.
این ترجمه برای استفادهٔ آموزشی تهیه شده است. انتشار عمومی، فروش یا بازنشر آن باید با رعایت حقوق مؤلف و ناشر و اخذ مجوزهای لازم انجام شود.