যা যা আছে

Feed-forward ANN - 4
(Encoder থেকে Decoder)


আগের পোস্টের শেষে আমরা একটা ঝামেলায় পড়েছিলাম। আমাদের বানানো net1-কে initialize করতে গিয়ে দেখেছি, সেটা করা যায়নি। অথচ, NetTrain যে নেটটা বানিয়েছে, সেটা দিব্যি কাজ করছে। এর কারণ কি? Initialize করা যায় না, এমন নেটকে NetTrain train করালো কি করে? ব্যাপারটা বুঝতে গেলে net1 তৈরি করার সময় আমরা যে output টা পেয়েছি, তার সঙ্গে trained net, অর্থাৎ trnet1-এর output-এর পার্থক্য কি, দেখা দরকার।

 

দেখো, net1 এর input দেখাচ্ছে, ‘array’, আর trnet1-এর input, ‘vector (size: 4)’। আবার, net1 এর output, ‘vector (size: 3)’, আর trnet1-এর output, ‘class’। খুবই স্বাভাবিক! net1 তৈরি করার সময় তো আমরা input কি হবে তা নির্দেশ করিনি! Output যে একটা size – 3 vector হবে, সেটা SoftmaxLayer এর আগের layer-এর size দেখেই বোঝা যাচ্ছে। কিন্তু সেই সংখ্যাগুলো যে class নির্দেশ করবে, তা তো আমরা বলে দিইনি। সেটা না হয় আমরা করতে শিখবো একটু পরেই, কিন্তু NetTrain কি করে বুঝলো, যে input-টা একটা size – 4 vector হবে? বা, output হবে class? এর কারণ, তুমি যখন NetTrain কে training data দিয়ে train করতে বলেছো, তখনই data-র গঠন দেখে ও বুঝে নিয়েছে, input কি হবে, আর output-কেই বা কি করে পড়তে হবে। মোদ্দা কথা, Mathematica আমাদের মতো অন্যমনস্ক কাঁচা খেলোয়াড়দের ভুলগুলো নিজেই শুধরে নিয়েছে।

Tensor: যদি ঠিকভাবে একটা নেট তৈরি করার চেষ্টা করতে হয়, তবে তার আগে জানতে হবে, input আর output কিভাবে তৈরি করতে হয়। যেকোন artificial neural network, Tensor নিয়ে কাজ করে। Tensor কি না জানা থাকলে, Google করো। সামান্য ঝালাই করার দরকার থাকলে এই ভিডিওটা দেখে নাও:


বিভিন্ন রকম tensor-এর কিছু উদাহরণ:

Rank 0 Tensor (representation: scalar): 0.0
Rank 1 Tensor (representation: vector): {0.0, 1.0}
Rank 2 Tensor (representation: matrix): {{1., 2., 3.} , {3., 2., 1.}}
Rank \( n \) Tensor : {... {... {1., 2., 3.}...}...}

বিভিন্ন ধরণের input-কে আমরা নানাভাবে tensor-এর চেহারায় প্রকাশ করতে পারি। উদাহরণ? আমাদের নিজেদের উদাহরণেই, ফুলের 4 রকমের feature-এর একটা করে সমষ্টি রয়েছে, যাদের আমরা size-4 vector দিয়ে represent করতে পারি। যেকোন \( n \) dimension-এর coordinate system-এ কোন coordinate-কে প্রকাশ করা যেতে পারে একটা size-\( n \) vector দিয়ে।


যেকোন সাদা-কালো ছবিতে (gray-scale ছবি, black & white নয়; দ্বিতীয় ধরণের ছবিকে binary image বলে) প্রতি pixel এ রঙের পরিমাণ পুরোপুরি সাদা থেকে পুরোপুরি কালোর মধ্যে ঘোরাফেরা করে। ধবধবে সাদা বা কুচকুচে কালো, যেকোন একটাকে 1, আর অন্যটাকে 0 বললে বোঝাই যাচ্ছে যেকোন pixel-এর value 0 থেকে 1 কোন একটা real number হবে। এই পোস্টের একদম প্রথমে যে ছবিটা আছে, সেখানে দেখো একটা হাতে লেখা ইংরেজি 8 (অথবা বাংলা ৪)-এর ছবিতে সাদাকে 1 আর কালোকে 0 ধরে সব pixel এর একটা matrix বানানো হয়েছে। ব্যস্, এই representation-এ এই ছবিটা একটা Rank-2 tensor। নীচে আরও একটা উদাহরণ দেওয়া হলো:


আমরা আগের একটা পোস্টে জেনেছি, রঙিন ছবিকে নানারকম representation-এ প্রকাশ করা যায়, যেমন ‘RGB’ বা ‘HSV’। কম্পিউটারে যে পদ্ধতিতে কোন রঙিনকে প্রকাশ করে save করা হয়, তাকে তার color-space বলে (এই লেখার সময় অবধি, gray-scale-সহ Mathematica আট রকমের standard color-space চেনে আর প্রয়োজন পড়লে যেকোন user তার পছন্দমতো custom color-space বানিয়ে নিতে পারে)। উদাহরণ হিসেবে, ‘RGB’ color-space-এর একটা ছবিতে প্রতি pixel তৈরি হয় 3-টে সংখ্যা দিয়ে: \( \{ r, g, b\} \)। নীচের উদাহরণে দেখো, আমরা এরকম বেশ কিছু color define করেছি এই তিনটে সংখ্যার নানা রকমফের দিয়ে, তারপর সেই রঙগুলো দিয়ে অনেকগুলো চাকতি তৈরি করেছি:

In[1]:= rgbcolors = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}, {1, 1, 0}, {0, 1, 1}, {1, 0, 1}, {0.1, 0.2, 0.1}};
In[2]:= Row[Graphics[{RGBColor@@#, Disk[ ]}] & /@ rgbcolors]
Out[2]:=

তাহলে বোঝা যাচ্ছে, একটা রঙিন ছবির RGB representation যেন আসলে তিনটে gray-scale ছবি, একটার ওপর একটা চাপানো। এদের একেকটাকে চলতি কথায় একেকটা color channel বলে। নীচের উদাহরণে একটা রঙিন ছবিকে প্রথমে তার তিনটে color channel-এ ভেঙে, তারপর তাদের order-টা নানারকমভাবে বদলে দিয়ে আবার মেশানো হয়েছে। ফলে আমরা অনেকরকম false-color ছবি তৈরি করতে পেরেছি:

In[3]:= labImage = Entity[ "DogBreed", "LabradorRetriever"][ EntityProperty[ "DogBreed", "Image"] ]
In[4]:= colsepLI = ColorSeparate[labImage]
In[5]:= MapThread[ImageMultiply, {colsepLI, {Red, Green, Blue}}]
In[6]:=  Map[ColorCombine, Permutations[colsepLI]]

Out[3]:=

Out[4]:=

Out[5]:=

Out[6]:=


অতএব, একটা রঙিন ছবিকে আমরা একটা Rank-3 tensor হিসেবে প্রকাশ করতে পারি, যার প্রথম index-টা 1 থেকে 3 এর মধ্যে ঘোরাফেরা করে (RGB, HSV বা এমন কোন color-space, যাতে তিনটে অংশ আছে; “CMYK” (cyan, magenta, yellow, black) বা “RGBA” (red, green, blue, alpha) – এই ধরণের color-space-এর জন্যে প্রথম index-এর range হবে 1 থেকে 4)। সেটাই করা যাক তবে:


Mathematica-য় কোন tensor-এর rank জানতে ArrayDepth function-টা ব্যবহার করতে হবে। বেশ, নানারকম রঙচঙে উদাহরণের সাহায্যে বোঝা গেলো, নানারকমের data-কে কি করে tensor form-এ প্রকাশ করা যায়। কিন্তু তা কি আমাদের হাতে করে করতে হবে প্রতিবার training এর আগে? নাকি data-কে save করে রাখার সময়েই tensor form-এ save করতে হবে? সে তুমি করতেই পারো, কিন্তু তার থেকে অনেক সহজ উপায় আছে। আর NetTrain তোমার অজান্তে সেইরকমই একটা কাজ করছিলো।

NetEncoder আর NetDecoder: নেট বানানোর সময় প্রথমেই যে দু’খানা layer অপরিহার্য, তারা হলো input আর output layer। মনে করে দেখো, Feed-Forward ANN-2 পোস্টে আমরা যখন প্রথম linear layer বানিয়েছিলাম, তখন তার weight matrix-এর \( W_{i,j} \) element-টা ছিলো \( i \)-তম neuron-এর \( j \)-তম input-এর weight। Input layer-এর dimension যদি আমরা নেটকে বলে না দিই, তবে ও \( W \)-র dimension জানবেই বা কি করে, আর তাকে initialize-ই বা করবে কি করে? ঠিক এই কারণে, আমাদের তৈরি net1-কে আমরা initialize করতে পারিনি। Output-এর ক্ষেত্রেও তা-ই। যদি বলে দিতাম, দেখো ভায়া, তোমায় আমি 4-টে সংখ্যা দেবো, তুমি তাদের একটা size-4 vector হিসেবে দেখো, আর label হিসেবে থাকবে 3-খানা class (ওই যে, setosa, versicolor আর virginica), যাদের একসাথে তুমি একটা size-3 vector হিসেবে দেখো (ঠিক ওই RGB vector-এর মতো: যে label-এ versicolor থাকবে, সেটা হবে {0., 1., 0.}, আবার যেখানে virginica থাকবে, সেটা হবে {0., 0., 1.} – এইরকম…), তাহলে প্রথম থেকেই আমাদের নেট জানতো, যে input-এ 3-টে সংখ্যা, আর output-এ একটা class থাকবে। যে function দিয়ে আমরা এই input/output \( \to \) tensor রূপান্তরটা করে ফেলতে পারি, তাদের বলে NetEncoder আর NetDecoder

NetEncoder-এর কাজ হলো আমাদের ঠিক করে দেওয়া কোন এক বিশেষ ধরণের input নিয়ে তাকে উপযুক্ত কোন এক ধরণের tensor-এ বদলে ফেলা। এরা সাধারণ Mathematica function-এর মতোই কাজ করে, কিন্তু এদের বিশেষত্ব হলো, কোন net-এর “Input” option-এ এদের একজনকে বসিয়ে দিলে, training data-কে সেইমতো automatically বদলে ফেলে, যাতে সেই network নির্দ্বিধায় training শেষ করতে পারে। একটা উদাহরণ দেখো:

In[7]:= enc = NetEncoder[{"Image", {32, 32}, ColorSpace -> "RGB"}]
Out[7]:=

এখানে আমরা একটা encoder বানিয়েছি, যা কোন রঙিন ছবি পেলে, তাকে RGB color-space-এ (\( 32\times 32 \)) মাপের 3-টে channel-এ ভেঙে ফেলে, ফলে, তৈরি হয় একটা Rank-3 tensor, যার dimension (\( 3\times 32\times 32 \))।

In[8]:= i = Entity[ "Species", "Species:FelisCatus" ] [ EntityProperty[ "Species", "Image" ] ]
In[9]:= ImageDimensions[ i ]
In[10]:= Dimensions[ enc[ i ] ]
In[11]:= ArrayPlot /@ enc[ i ]

Out[8]:=

Out[9]:= {150, 100}
Out[10]:= {3, 32, 32}
Out[11]:=

খেয়াল করো, আমরা আসল ছবির থেকে অনেক ছোট মাপের একটা tensor বানিয়েছি। এর ফলে যা যা compression করা দরকার, তা NetEncoder নিজেই করে নিয়েছে, আমাদের আর মাথাব্যাথা নেই।

Input থেকে tensor-এ encode যদি করা যায়, তবে tensor থেকে output decode-ও করতে পারা সম্ভব। সেটাই করে NetDecoder। মাঝে কোন net-এরও প্রয়োজন নেই, NetEncoder দিয়ে encode করা tensor-কে যদি সঠিকভাবে তৈরি করা NetDecoder-এর ওপর চাপিয়ে দাও, আগের ছবিই ফিরে পাবে (যদি না আগের উদাহরণের মতো compress করে থাকো, সেক্ষেত্রে compressed ছবিই পাবে)। এইরকম একটা উদাহারণ আজকের notebook-এর 3-নম্বর উদাহরণে দেওয়া আছে। এমন encoder-decoder-এর যুগলবন্দী আমরা আবার দেখবো AutoEncoder শেখার সময়।


কিন্তু আমাদের কাজে এই মুহূর্তে NetDecoder-এর যে বিশেষত্বটা সবথেকে বেশি প্রয়োজন, তা হলো output tensor-কে class-এর combination হিসেবে ভাবতে পারা। নীচের উদাহরণে দেখো, আমরা দু’রকম label দিয়ে দু’টো class তৈরি করে সেটা দিয়ে একটা decoder বানিয়েছি। এবার ওকে যেকোন একটা size-2 vector-এর ওপর চাপালেই ও ভাববে, এ তো দেখছি একটা vector, যার মাপ ঠিক আমার কাছে থাকা class-এর লিস্টের মাপের সমান, তাহলে বোধহয় এর এক একটা element (যদি numeric value হয়, তবেই) এক একটা class-এর probability বোঝাচ্ছে। সঙ্গে সঙ্গে ও যত নম্বর element-টা maximum, তত নম্বর class-টা output-এ বসিয়ে দেবে। শুধু তা-ই নয়, ওর কাছে চাইলে কোন class-এর কত probability, তাও চাইলে জিজ্ঞেস করে জেনে নেওয়া সম্ভব।

In[12]:= dec = NetDecoder[{"Class", {"dog", "cat"}}]
In[13]:= dec[{0.1, 0.9}]
In[14]:= dec[{0.1, 0.9}, "Probabilities"]

Out[12]:=

Out[13]:= cat
Out[14]:= <|"dog" -> 0.1, "cat" -> 0.9|>



বেশ, এই সমস্ত তথ্যসমৃদ্ধ হয়ে আমরা কি বুঝলাম? বুঝলাম, যে আমাদের তৈরি net1-এর সঙ্গে একটা করে encoder আর decoder যোগ করতে হত। তাহলেই, এই training নামক অদ্ভুত ঘটনার ওপর আমাদের কিছু নিয়ন্ত্রণ থাকতো, আর net-টাকে প্রথম থেকেই initialize করে আমরা তার ওপর নানা পরীক্ষা-নিরীক্ষা করতে পারতাম:

In[15]:= net2 = NetChain[{10, LogisticSigmoid, 3, SoftmaxLayer[ ]}, "Input" -> Length[{"Sepal Length", "Sepal Width", "Petal Length", "Petal Width"}], "Output" -> NetDecoder[{"Class", {"setosa", "versicolor", "virginica"}}]]
In[16]:= (NetInitialize@net2)[dataTest[[-1, 1]]]

Out[15]:=

Out[16]:= setosa


আজ এই পর্যন্তই। পরদিন আমরা জানবো training-এর হাল-হকিকৎ। অন্যান্য দিনের মতোই, সঙ্গের notebook-এ আরও নানারকম উদাহরণ করে দেখানো আছে, কিছু coding-tips-ও আছে। Aloha!

No comments