TFRecords و tf.train.Example و نحوه کار با آنها
در مقاله آموزشی پیشرو به شما نشان میدهیم که چگونه میتوان دادههایی که فرمت TFRecords دارند را ذخیره کرد و خواند. فرمت TFRecord فرمت محبوب تنسورفلو است. به همین منظور از پروتکل بافر پیام tf.train.Example استفاده خواهیم کرد.
در مقاله آموزشی حاضر از Tensorflow 2.0 استفاده خواهیم کرد، برای نصب آن به روش زیر عمل کنید:
!pip install tensorflow==2.0.0-beta1 import tensorflow as tf print(tf.__version__)
پروتکل بافرهای Tensorflow
پروتکل بافرها فرمت جدیدی برای ذخیره کردن و خواندن دادهها هستند. ساختار داده در یک پروتکل بافر پیام در فایل .proto نوشته میشود. فایل .proto شیوه ذخیرهسازی دادهها را به کامپایلر پروتکل بافر اطلاع میدهد. در گام بعدی کامپایلر یک کلاس ایجاد میکند که دادهها را کدگزاری و تجزیه و تحلیل میکند. این کلاس برای دریافت و ذخیره دادهها متدهای مختلفی به کار میبندد. این کلاس را به نوعی میتوان نمونهای از XML در نظر گرفت. در مقابل میتوانید نمونهای از پروتکل بافر پیام را مشاهده کنید:
message Person { required string name = 1; required int32 id = 2; optional string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { required string number = 1; optional PhoneType type = 2 [default = HOME]; } repeated PhoneNumber phone = 4;
- هر نوع پیام یک یا بیش از یک فیلد دارد که با اعداد مختلف عددگذاری شدهاند. هر یک از فیلدها هم یک نام و یک نوع داده دارد. نوع دادهها میتوانند عدد ( صحیح یا اعشاری) ، بولی، رشته (string)، بایتهای خام و حتی (همانند مثال فوق) پروتکل بافر پیام باشند و شما میتوانید با استفاده از آنها دادههایتان را به صورت سلسله مراتبی مرتب کنید.
برای انجام پروژه پیشرو نیازی نیست خودتان یک پروتکل بافر پیام بنویسید و به همین دلیل ما هم از معرفی کامل پروتکل بافرها صرف نظر میکنیم. همینکه با پروتکل بافرهای پیام مقابل ( خاص Tensorflow) آشنایی داشته باشید، کافی است:
- train.BytesList ، tf.train.FloatList ، tf.train.Int64List : این پروتکل بافرهای پیام فقط یک فیلد دارند که “value” نام دارد و میتواند دادههایی با فرمت مقابل را در خود ذخیره کند:
tf.train.BytesList :
– رشته
– بایت
tf.train.FloatList :
– float (float32)
– double (float64)
: tf.train.Int64List
– bool
– enum
– int32
– uint32
– int64
– uint64
این سه پیام نوع داده هستند.
2- tf.train.Feature: این پیام فقط یک فیلد دارد که یکی از سه نوع داده فوق را میپذیرد. این پیام را میتوان یک ویژگی واحد برای یک نقطهداده در نظر گرفت.
3- tf.train.Features : این پروتکل بافر پیام یک نگاشت {“string”: tf.train.Feature} است. این پیام فهرستی از ویژگیهای یک نقطه داده مشخص است.
4- tf.train.Example : این پروتکل بافر پیام فقط یک فیلد از نوع tf.train.Feature دارد که ‘features’ نامیده میشود. این پیام انتزاعی از یک نقطه داده واحد است. در واقع این پیام پوششی پیرامون tf.train.Features است.
برای تبدیل یک نقطه داده یا یک ردیف از دادهها به یک tf.train.Example باید یک tf.train.Feature برای هر یک از ویژگیهای دادهها ایجاد کنید. به همین منظور در سند TensorFlow یک تابع میانبُر معرفی شده:
1 |
def _bytes_feature(value): |
2 | “””Returns a bytes_list from a string / byte.””” |
3 | # If the value is an eager tensor BytesList won’t unpack a string from an EagerTensor. |
4 | if isinstance(value, type(tf.constant(0))): |
5 | value = value.numpy() |
6 | return tf.train.Feature(bytes_list=tf.train.BytesList(value=[value])) |
7 | |
8 | def _float_feature(value): |
9 | “””Returns a float_list from a float / double.””” |
10 | return tf.train.Feature(float_list=tf.train.FloatList(value=[value])) |
11 | |
12 | def _int64_feature(value): |
13 | “””Returns an int64_list from a bool / enum / int / uint.””” |
14 | return tf.train.Feature(int64_list=tf.train.Int64List(value=[value])) |
ورودی هر یک از این توابع یک مقدار عددی Scalar input value
است و یک tf.train.Feature شامل یکی از سه نوع فوق باز میگردانند. به مثال مقابل توجه کنید:
#strings needs to be converted into bytes. print(_bytes_feature(b’some string’)) print(_float_feature(0.5)) print(_int64_feature(True)) print(_int64_feature(1))
و خروجی آن:
bytes_list { value: “some string” } float_list { value: 0.5 } int64_list { value: 1 } int64_list { value: 1 }
با استفاده از تابع tf.io.serialize_tensor نیز میتوان ویژگیهای غیرعددی را به رشتههای باینری تبدیل کرد. برای اینکه رشته باینری را مجدداً به حالت تنسور برگردانید، از تابعtf.io.parse_tensor استفاده کنید:
import numpy as np a = np.random.randn(2,2) _bytes_feature(tf.io.serialize_tensor(a))
در صورتیکه پیام tf.train.Example را از دادهها ایجاد کنیم:
برای هر یک نقطهدادهها طبق مراحل زیر عمل کنید:
- برای هر یک از ویژگیها، البته با توجه به نوع آن، یکی از سه تابع میانبر فوق را اجرا کنید تا یک پیام train.Feature ایجاد شود.
- یک دیکشنری ایجاد کنید که در آن هر کلید (Key) نام یک ویژگی و مقادیر آن پیامهای train.Feature هستند (که در گام اول ایجاد کردیم). دیکشنری اینچنین ظاهری خواهد داشـت:
feature = { |
‘feature0’: _int64_feature(feature0), |
‘feature1’: _int64_feature(feature1), |
‘feature2’: _bytes_feature(feature2), |
‘feature3’: _float_feature(feature3), |
} |
- سپس یک پیام train.Example از دیکشنری ( که در گام دوم ایجاد کردیم) ایجاد کنید. فراموش نکنید که پیام tf.train.Example فقط یک فیلد از نوعtf.train.Features دارد. به همین دلیل ابتدا پیام tf.train.Features را از دیکشنری ایجاد کنید و با استفاده از آن یک پیام tf.train.Example ایجاد کنید. چیزی در نهایت ایجاد میشود اینگونه خواهد بود:
example_proto = tf.train.Example(features = |
tf.train.Features(feature=feature)) |
برای اینکه با ارائه یک مثال این فرایند را به شما نشان دهیم، یک دیتاست ساختگی ایجاد میکنیم. این دیتاست 4 ویژگی دارد.
import numpy as np # The number of observations in the dataset. n_observations = 1000 # Boolean feature, encoded as False or True. feature0 = np.random.choice([False, True], n_observations) # Float feature feature1 = np.random.randn(n_observations) # String feature strings = np.array([b’cat’, b’dog’]) feature2 = np.random.choice(strings, n_observations) # Non-scalar Float feature, 2x2 matrices sampled from a standard normal distribution feature3 = np.random.randn(n_observations, 2, 2 dataset = tf.data.Dataset.from_tensor_slices((feature0, feature1, feature2, feature3))
سپس طبق مراحل زیر یک تابع بنویسید تا یک tf.train.Example ایجاد کنید:
def create_example(feature0, feature1, feature2, feature3): feature = { 'feature0': _int64_feature(feature0), 'feature1': _float_feature(feature1), 'feature2': _bytes_feature(feature2), 'feature3': _bytes_feature(feature3), } # Create a Features message using tf.train.Example. example_proto = tf.train.Example(features=tf.train.Features(feature=feature)) return example_proto
در ادامه از اولین نقطه داده یک نمونه پیام ایجاد میکنیم:
for feature0, feature1, feature2, feature3 in dataset.take(1): example_proto = create_example(feature0, feature1, feature2, tf.io.serialize_tensor(feature3)) print(example_proto)
به دلیل اینکه feature3 یک ویژگی غیر عددی است با استفاده از tf.io.serialize_tensor آن را به یک بایت-رشته تبدیل کردیم.
اکنون شیوه ایجاد پیامهای tf.train.Example را از دادهها میدانید. در قدم بعدی به شیوه نوشتن آن را در یک فایل TFRecord و نحوه خواندن آنها را به شما نشان میدهیم.
TFRecords
- برای خوانش راحتتر دادهها میتوانید آنها را سریالسازی کرده و در مجموعهای از فایلها (100 تا 200 مگابایتی) ذخیره کنید، در این صورت میتواند هر کدام از آنها را به صورت خطی بخوانید. به ویژه اگر دادهها در سراسر یک شبکه پخش شده باشند، این روش مفید خواهد بود. این روش در کَش کردن عملیاتهای پیشپردازش داده نیز کاربرد دارد. فرمت TFRecord فرمت سادهای برای ذخیره مجموعهای از رکوردهای باینری است.
هر فایل TFRecord از یک توالی از رکوردها تشکیل شده است. در این توالی هر یک از رکوردها یک رشته-بایت است. لزومی ندارد در فایلهای TFRecords از پیامهای tf.train.Example استفاده کنیم اما در این مقاله آموزشی از آنها استفاده خواهیم کرد. در قسمت بالا توضیح دادیم که چگونه میتوان پیامهای tf.train.Example را از دادهها ایجاد کرد. برای اینکه بتوانیم این پیامها را با فرمت TFRecords بنویسیم، باید اول آنها را به رشته-بایت تبدیل کنیم. به همین منظور، میتوانیم از متد SerializeToString() استفاده کنیم. طبق مراحل زیر میتوانیم تابع را به روزرسانی کنیم و پیام Example را به رشته-بایت تبدیل کنیم:
def serialize_example(feature0, feature1, feature2, feature3): feature = { 'feature0': _int64_feature(feature0), 'feature1': _float_feature(feature1), 'feature2': _bytes_feature(feature2), 'feature3': _bytes_feature(feature3), } # Create a Features message using tf.train.Example. example_proto = tf.train.Example(features=tf.train.Features(feature=feature)) return example_proto.SerializeToString()
نمونه سریالسازی شده به شکل زیر خواهد بود:
for feature0, feature1, feature2, feature3 in dataset.take(1): serialized_example = serialize_example(feature0, feature1, feature2, tf.io.serialize_tensor(feature3)) print(serialized_example)
و خروجی آن:
b'\n~\n\x11\n\x08feature0\x12\x05\x1a\x03\n\x01\x00\n\x14\n\x08feature1\x12\x08\x12\x06\n\x04J\xa8\x1e\xbf\n\x13\n\x08feature2\x12\x07\n\x05\n\x03dog\n>\n\x08feature3\x122\n0\n.\x08\x02\x12\x08\x12\x02\x08\x02\x12\x02\x08\x02" <\xec#S\x08\x9e\xd3\xbfWbu\xb16\x9e\xf3?\xcej\xb6&\x8b$\xf1\xbf<i\x8eD\x81\t\xd2\xbf'
نوشتن دادهها در TFRecords
برای اینکه بتوانیم دادهها را در یک فایل TFRecords بنویسیم باید طبق مراحل فوق هر یک از نقطهدادهها را به یک رشته-بایت تبدیل کنیم و با استفاده از tf.io.TFRecordsWriter آن را در فایل بنویسیم:
file_path = 'data.tfrecords' with tf.io.TFRecordWriter(file_path) as writer: for feature0, feature1, feature2, feature3 in dataset: serialized_example = serialize_example(feature0, feature1, feature2, tf.io.serialize_tensor(feature3)) writer.write(serialized_example)
در نتیجه فایل data.tfrecords در یک مسیر مشخص ایجاد میشود که در مورد فوق همان دیکشنری است.
یکی از مکانهای مناسب برای اجرای برخی از عملیاتهای پیش پردازش بر روی دادهها، از جمله داده افزایی Data augmentation این است که از قبل نمونه را سریالسازی کرده و در فایل بنویسید. در ادامه مثالی از آن را با ذکر یک نمونه تصویری نشان خواهیم داد.
خواندن فایل TFRecords
در این قسمت نحوه خواندن رکوردهایی که ایجاد کردهایم را به شما نشان خواهیم داد. به همین منظور ابتدا یک tf.data.TFRecordDataset از فهرست مسیرهای فایل TFRecord ایجاد میکنیم.
file_paths = [file_path] # We have only one file tfrecord_dataset = tf.data.TFRecordDataset(file_paths)
در نتیجه تمامی نقطهدادههایی که در دیتاست قرار دارند به رشته-بایتهای خامی تبدیل میشوند که تابع serialize_example بازگردانده است. تابع پیشرو یکserialized_example را میخواند و با استفاده از توصیف ویژگی آن را تجزیه و تحلیل میکند.
def read_tfrecord(serialized_example): feature_description = { 'feature0': tf.io.FixedLenFeature((), tf.int64), 'feature1': tf.io.FixedLenFeature((), tf.float32), 'feature2': tf.io.FixedLenFeature((), tf.string), 'feature3': tf.io.FixedLenFeature((), tf.string), } example = tf.io.parse_single_example(serialized_example, feature_description) feature0 = example['feature0'] feature1 = example['feature1'] feature2 = example['feature2'] feature3 = tf.io.parse_tensor(example['feature3'], out_type = tf.float64) return feature0, feature1, feature2, feature3
اکنون میتوانیم این تابع را بر روی tfrecord_dataset اجرا کنیم و ویژگیهای مورد نظرمان را برگردانیم.
parsed_dataset = tfrecord_dataset.map(read_tfrecord) for data in parsed_dataset.take(2): print(data) #Output (<tf.Tensor: id=22470, shape=(), dtype=int64, numpy=0>, <tf.Tensor: id=22471, shape=(), dtype=float32, numpy=-0.6197554>, <tf.Tensor: id=22472, shape=(), dtype=string, numpy=b'dog'>, <tf.Tensor: id=22473, shape=(2, 2), dtype=float64, numpy= array([[-0.30652054, 1.22612638], [-1.07142177, -0.28183014]])>) (<tf.Tensor: id=22478, shape=(), dtype=int64, numpy=1>, <tf.Tensor: id=22479, shape=(), dtype=float32, numpy=-0.23810087>, <tf.Tensor: id=22480, shape=(), dtype=string, numpy=b'cat'>, <tf.Tensor: id=22481, shape=(2, 2), dtype=float64, numpy= array([[ 0.60822147, -0.77295083], [ 0.10305338, -1.50613709]])>)
ذکر مثال با یک نمونه تصویری
در این قسمت مثالی با یک نمونه تصویری ارائه میدهیم. من از دیتاست سگها و گربهها استفاده خواهم کرد و برای سهولت فقط از 100 تصویر اول هر دسته استفاده خواهم کرد و آنها را در پوشهای به نام “data” در دیکشنری قرار میدهیم.
در قدم اول فهرستی از مسیرهای تصاویر ایجاد میکنیم.
import glob data_dir = 'data/' image_paths = glob.glob(data_dir + '*.jpg')
100 تصویر اول، تصویر گربه است و 100 تصویر بعدی، تصویر سگ. تصاویر گربه را با 0 و تصاویر سگ را با 1 برچسبگذاری میکنیم.
labels = np.append(np.zeros(100, dtype=int),np.ones(100, dtype=int))
حالا 9 تصویر اول را نگاه میکنیم.
import matplotlib.pyplot as plt plt.figure(figsize=(10,10)) for i, path in enumerate(image_paths[:9]): img = tf.keras.preprocessing.image.load_img(path) plt.subplot(3,3,i+1) plt.imshow(img) plt.show()
حالا میتوانیم TFrecords را ایجاد کنیم. به غیر از توصیفات ویژگی، تابع serialize_example همانند فوق است.
def serialize_example(image, label, image_shape): feature = { 'image': _bytes_feature(image), 'label': _int64_feature(label), 'height': _int64_feature(image_shape[0]), 'width': _int64_feature(image_shape[1]), 'depth': _int64_feature(image_shape[2]), } # Create a Features message using tf.train.Example. example_proto = tf.train.Example(features=tf.train.Features(feature=feature)) return example_proto.SerializeToString()
و بر روی اعضای دیتاست یک حلقه تعریف میکنیم و در فایل مینویسیم.
tfrecord_dir = 'tfrecords/data.tfrecords' with tf.io.TFRecordWriter(tfrecord_dir) as writer: for image_path, label in zip(image_paths, labels): img = tf.keras.preprocessing.image.load_img(image_path) img_array = tf.keras.preprocessing.image.img_to_array(img) img_array = tf.keras.preprocessing.image.random_zoom(img_array, (0.5,0.5), row_axis=0, col_axis=1, channel_axis=2) img_bytes = tf.io.serialize_tensor(img_array) image_shape = img_array.shape example = serialize_example(img_bytes, label, image_shape) writer.write(example)
در حلقه بالا، ابتدا تصویر را به یک آرایه numpy تبدیل میکنیم و سپس برخی از عملیاتهای داده افزایی را اجرا میکنیم که در این مورد را random_zoom اجرا خواهیم کرد. سپس آرایه numpy را به رشته-بایت تبدیل میکنیم و آن را به همراه برچسبها و ابعاد تصویر در یک فایل مینویسیم.
برای اینکه بتوانیم دادهها را در فایل tfrecords بخوانیم باید طبق مراحل فوق عمل کنیم.
def read_tfrecord(serialized_example): feature_description = { 'image': tf.io.FixedLenFeature((), tf.string), 'label': tf.io.FixedLenFeature((), tf.int64), 'height': tf.io.FixedLenFeature((), tf.int64), 'width': tf.io.FixedLenFeature((), tf.int64), 'depth': tf.io.FixedLenFeature((), tf.int64) } example = tf.io.parse_single_example(serialized_example, feature_description) image = tf.io.parse_tensor(example['image'], out_type = float) image_shape = [example['height'], example['width'], example['depth']] image = tf.reshape(image, image_shape) return image, example['label']
در گام بعدی این تابع را بر روی دیتاست tfrecords اجرا میکنیم و دوباره 9 تصویر اول را نگاه میکنیم.
tfrecord_dataset = tf.data.TFRecordDataset(tfrecord_dir) parsed_dataset = tfrecord_dataset.map(read_tfrecord) plt.figure(figsize=(10,10)) for i, data in enumerate(parsed_dataset.take(9)): img = tf.keras.preprocessing.image.array_to_img(data[0]) plt.subplot(3,3,i+1) plt.imshow(img) plt.show()
البته توجه داشته باشید پیش از اینکه دادهها را در فایل بنویسیم، tf.keras.preprocessing.image.random_zoom را اجرا میکنیم و به همین دلیل تصاویر را به صورت تصاویر بزرگنمایی (zoom-in) مشاهده میکنیم.
چکیده
TFRecords یک فرمت بهینه است که میتوانیم از آن در فرایندهای پردازش دادهها استفاده کنیم و پیامهای پروتکل بافر روش مناسبی برای نوشتن دادهها در فایلهای TFRecord هستند. استفاده از فایلهای TFRecords میتواند سرعت عملیاتها را افزایش دهد به ویژه اگر در فرایند آموزش با مشکل بارگذاری دادهها در مواجه هستید.