پایتورچ و تنسرفلو: دو فلسفه متفاوت در مهندسی نرم افزار

بر طبق آخرین آمار کنفرانس های یادگیری ماشین پایتورچ محبوب ترین چارچوب یادگیری عمیق است و بر اساس بنچ مارک های مستقل سرعت آن بیشتر از تنسرفلو می دانند. پایتورچ تا سه سال پیش حتی وجود نداشت! اما ناگهان ظهور پیدا کرد و به طرز شگفت انگیزی رقیب بلامنازع خود یعنی تنسرفلو را از میدان به در کرد!‌ تنسرفلویی که توسط بزرگترین غول نرم افزاری جهان یعنی گوگل سه سال حتی قبل از تولد پایتورچ منتشر شد و برای نسل جدید سخت افزارهای شرکت گوگل یعنی TPUها بهینه شد. کنفرانس برگزار شد. کتاب برای آن نوشته شد و خلاصه تمام تبلیغاتی که مثلا برای یک برند لوکس بنظرتان برسد برایش انجام دادند. اما چه چیزی باعث شد از پایتورچی که حتی اوایل مشخص نبود چه شرکتی پشت آن باشد عقب افتاد؟! دلایل زیادی می توان ذکر کرد اما شاید مهمترین آن ها موارد زیر باشد:تنسرفلو برای مدت طولانی یکه تاز بود هرچند چهارچوب های دیگری مثل caffe هم وجود داشتند اما ساپورت تنسرفلو برای اجرا در پایتون آن را یکتا میکرد. تنسرفلو اما مسیری را در توسعه نرم افزاری خود پیش گرفت که بسیاری از کاربران آن را نا امید میکرد. یک ضرب ماتریسی ساده در تنسرفلو نیاز به فراخوانی توابعی داشت که «مساله» ی کاربر دیتاساینتیست نبود. تعریف گراف و بعد اجرای آن در سشن ها و غیره. این عدم شناخت از زمینه ی کاری کاربران اولین مشکل تنسرفلو بود. چرا که توسعه یادگیری ماشین بر اساس تنسرفلو را بسیار کند می کرد.مشکل دوم تنسرفلو اضافه کردن ویژگی ها و توابع به صورت نامنظم و دیوانه وار بود، طوری که ویژگی هایی در آن از چندیدن سال پیش وجود داشت که هنوز هم در پایتورچ وجود ندارد. اما نکته دقیقا همینجاست. افزایش حجم کتابخانه بدون یک برنامه مشخص برای دسته بندی کردن آن ها به قیمت اضافه کردن ویژگی هایی که حتی یک درصد از کاربران برایشان مهم نیست یک استراتژی غلط در نرم افزار است. به طور مثال تنسرفلو از اعداد مختلط پشتیبانی می کند اگر شما در حوزه یادگیری عمیق کار کرده باشید تقریبا هیچ گاه با چنین تایپ داده ای برخورد نمی کنید. بنابراین سوالی که پیش می آید این است کتابخانه ای که هدف اصلی آن پیاده سازی شبکه های عصبی عمیق است چرا باید چنین ویژگی ای داشته باشد.تغییرات در مدیریت نسخه بندی تنسرفلو هم به همان اندازه بی نظم بود. شما در نسخه های قبل از ۲ میتوانستید یک تابع را در چندین جای مختلف پیدا کنید. برخی از این بخش ها deprecated شده بودند اما اینرسی ناشی از محیط های قدیمی (legacy) وجود آن ها را ضروری میکرد. ساختار بی نظمی که خود توسعه دهندگان تنسرفلو به آن اشاره میکنند و اساس طراحی مجدد API های سطح بالا به تنسرفلو ۲ شد.وضعیت دشوار استفاده از tensorflow منجر به ساخت کتابخانه هایی شد که این پیچیدگی را از کاربر نهایی مخفی میکنند که معروف ترین آن ها keras بود. با انتقال به tensorflow2 قرار شد که که این دو API متحد شوند اما در عمل استفاده از کراس بدون ارجاع به backend (که همان تنسرفلو باشد) غیر قابل استفاده است. این کار منجر به نقض اصل «جدا کردن لایه های انتزاع» در مهندسی نرم افزار می شود (levels of abstraction) و کد را غیر قابل فهم و دیباگ کردن آن را دشوار می کند. از طرفی این اتحاد خیالی بین این دو هم در عمل رخ نداد.مشکل دیگر تنسرفلو این بود که استفاده از GPU که اساسی ترین پلتفرم اجرایی آن است بسیار دشوار تر و همواره همراه با خطا است (این را از تجربه استفاده از تسرفلو به مدت چهار سال با انواع و اقسام کارت های گرافیک می گویم). نصب دیفالت تنسرفلو، بر خلاف پایتورچ، بر روی cpu است!! اما در پایتورچ حتی نصب جی پی یو همچنان امکان استفاده از cpu را میدهد که منطقی تر بنظر میرسد.برخلاف تمام اینها پایتورچ بازیگر صبور تری بود به این معنا که از ابتدا حداقل ها را در چارچوب خود وارد کرد اما سعی بر آن بود که تا حد امکان آن را pythonic نگه دارد. به این معنا که وقتی شما از پایتون استفاده می کنید انتظار دارید که کد ها شبیه به پایتون باشند نه یک wrapper از یک کد به زبان دیگر!! اگر یک تنسور تعریف می شود از ابتدا باید مقدار آن در زمان دبیاگ مشخص باشد. ویژگی واضحی که در تنسرفلو تا نسخه دو وجود نداشت!! پایتورچ ویژگی مهم دیگری را هم داشت که برگ برنده اصلی آن بود: تعریف شبکه ها بر اساس گراف های پویا (dynamic graph) که باعث می شد که کد به صورت عجیب و غیر قابل فهم بر اساس تعریف گراف استاتیک و بعد تعریف سشن و خوراندن داده ها به آن شبکه نباشد. برای این که منظور را متوجه شوید می توانید تفاوت پیچیدگی دو کد زیر را ببینید که دقیقا یک کار را می کنند!انجام یک عملیات ساده در تنسرفلو ۱.۱۵ و پایتورچ ۱.۴از طرفی پایتورچ با اضافه کردن بدون ملاحظه ویژگی های که یک کتابخانه دیگر می توانست آن را انجام دهد خود را سنگین و غیر قابل فهم نکرد. دوستی از من میپرسید: «چرا هنوز پایتورچ روندی برای خلاص شدن از دست حلقه ها بر روی داده های آموزش ندارد؟ چنین چیزی خیلی ساده است کافی است چیزی شبیه تابع fit در کراس استفاده شود که ده ها خط کد را که شامل حلقه هم هست را در یک خط انجام دهد.» من گفتم انجام چنین کاری به هیچ وجه واضح نیست چون باید callback های زیادی تعریف کرد و از طرفی چنین چیزی را کتابخانه های دیگر هم می توانند انجام دهند (ignite catalyst و غیره).مشخص است اضافه کردن چنین ویژگی ای برای گروه پایتورچ کمتر از یک هفته وقت میبرد اما انجام ندادن آن بهتر از انجام دادنش است!و در نهایت پایتورچ از بهترین تجارب از , DyNet, Chainer, torch, lua و در نهایت پدربزرگ تمام این چارچوب ها یعنی caffe استفاده کرد و در عمل آن ها را وارد کتابخانه خود کرد. استفاده از بهترین ویژگی های رقبا و مطالعه دقیق کاستی های آن ها پایتورچ را از تاخیر سه ساله ی ظهور در مقابل تنسرفلو ایمن کرد.داستان پایتورچ و تنسرفلو تقابل دو دیدگاه متفاوت در مهندسی نرم افزار است که در یکی درنظر نگرفتن جامعه کاربری، سنگین کردن کتابخانه با ویژگی های عجیب و در نهایت نداشتن برنامه مشخص برای معماری نرم افزار را در برابر یک مشی مینیمالیست که صبور و کند تر است را نشان می دهد. ادعای نگارنده این نیست که این مشی همواره و برای هر نرم افزاری بهترین راه حل است اما نباید فراموش کرد توسعه نرم افزار لزوما همیشه به معنای انجام یک کار فوق سریع نیست. صبر و دیدگاه پرنده به جای قورباغه هم همچنان می تواند برگ برنده یک گروه نرم افزاری باشد تا راه حل های فست فودی که در نهایت منجر به انقراض نرم افزار می شود.