অধ্যায় ২

Backpropagation algorithm কীভাবে কাজ করে

How the backpropagation algorithm works

গত অধ্যায়ে আমরা দেখেছি কীভাবে neural network gradient descent algorithm ব্যবহার করে নিজের weight ও bias শিখে নিতে পারে। তবে আমাদের ব্যাখ্যায় একটা ফাঁক ছিল — cost function-এর gradient কীভাবে compute করতে হয় তা আলোচনা করিনি। সেটা কিন্তু বেশ বড় ফাঁক! এই অধ্যায়ে আমি এমন একটা দ্রুত algorithm ব্যাখ্যা করব যা দিয়ে এই gradient হিসাব করা যায় — এই algorithm-কে বলা হয় backpropagation

Backpropagation algorithm প্রথম চালু হয়েছিল ১৯৭০-এর দশকে, কিন্তু এর গুরুত্ব পুরোপুরি অনুধাবিত হয়নি যতক্ষণ না David Rumelhart, Geoffrey HintonRonald Williams-এর ১৯৮৬ সালের একটি বিখ্যাত গবেষণাপত্র প্রকাশিত হয়। সেই পত্রটি এমন কয়েকটি neural network বর্ণনা করে যেখানে backpropagation শেখার আগের পদ্ধতিগুলোর তুলনায় অনেক দ্রুত কাজ করে, ফলে আগে যেসব সমস্যা অসমাধেয় ছিল সেগুলোতেও neural net ব্যবহার সম্ভব হয়। আজ backpropagation algorithm হলো neural network-এ শেখার মূল কর্মীঘোড়া (workhorse)।

এই অধ্যায়টি বইয়ের বাকি অংশের চেয়ে গাণিতিকভাবে বেশি জটিল। গণিত নিয়ে তুমি খুব উৎসাহী না হলে হয়তো অধ্যায়টা এড়িয়ে যেতে চাইবে, আর backpropagation-কে একটা black box ধরে নিয়ে তার খুঁটিনাটি উপেক্ষা করতে চাইবে। তাহলে সময় নিয়ে ওই খুঁটিনাটি পড়ার মানে কী?

কারণটা অবশ্যই — বোঝা। Backpropagation-এর কেন্দ্রে আছে cost function CC-এর partial derivative C/w\partial C / \partial w-এর একটা রাশি, যা network-এর যেকোনো weight ww (বা bias bb) সাপেক্ষে নেওয়া। এই রাশি আমাদের বলে দেয় weight ও bias বদলালে cost কত দ্রুত বদলায়। রাশিটা একটু জটিল হলেও এর একটা সৌন্দর্যও আছে — এর প্রতিটি উপাদানের একটা স্বাভাবিক, স্বজ্ঞাত ব্যাখ্যা আছে। তাই backpropagation কেবল শেখার একটা দ্রুত algorithm নয়। এটি আসলে আমাদের গভীর অন্তর্দৃষ্টি দেয় — weight ও bias বদলালে network-এর সামগ্রিক আচরণ কীভাবে বদলায়। সেটা বিস্তারিত পড়ার মতো মূল্যবান বিষয়।

এসব বলার পরও, তুমি যদি অধ্যায়টা চোখ বুলিয়ে যেতে চাও, বা সোজা পরের অধ্যায়ে চলে যেতে চাও, তাতেও সমস্যা নেই। বইয়ের বাকি অংশ আমি এমনভাবে লিখেছি যাতে backpropagation-কে black box ধরে নিলেও তা বোঝা যায়। অবশ্যই, বইয়ের পরের কিছু জায়গায় আমি এই অধ্যায়ের ফলাফলগুলোর কথা উল্লেখ করব। তবে সেসব জায়গায়ও সব যুক্তি অনুসরণ না করেই মূল উপসংহারগুলো বুঝতে পারবে।

Warm up: neural network-এর output হিসাব করার একটি দ্রুত matrix-ভিত্তিক পদ্ধতি

Backpropagation নিয়ে আলোচনার আগে চলো একটা দ্রুত matrix-ভিত্তিক algorithm দিয়ে warm up করি যা neural network-এর output হিসাব করে। আসলে এই algorithm আমরা গত অধ্যায়ের শেষদিকে সংক্ষেপে দেখেছিলাম, কিন্তু খুব দ্রুত বর্ণনা করেছিলাম, তাই এটা বিস্তারিতভাবে আবার দেখা সার্থক। বিশেষত, backpropagation-এ ব্যবহৃত notation-এর সাথে একটা পরিচিত প্রসঙ্গে স্বচ্ছন্দ হওয়ার এটা একটা ভালো উপায়।

শুরু করি এমন একটা notation দিয়ে যা network-এর weight-গুলোকে দ্ব্যর্থহীনভাবে নির্দেশ করতে দেয়। আমরা wjklw^l_{jk} ব্যবহার করব (l1)th(l-1)^{\rm th} layer-এর kthk^{\rm th} neuron থেকে lthl^{\rm th} layer-এর jthj^{\rm th} neuron-এর connection-এর weight বোঝাতে। যেমন নিচের diagram-এ দেখানো হয়েছে একটা network-এর দ্বিতীয় layer-এর চতুর্থ neuron থেকে তৃতীয় layer-এর দ্বিতীয় neuron-এর connection-এর weight:

দুটি layer-এর মধ্যে weight w^l_{jk}-এর notation
wjklw^l_{jk} notation: নিচের layer থেকে উপরের layer-এর একটি connection-এর weight।

এই notation প্রথমে একটু বেমানান মনে হয়, আর আয়ত্ত করতে কিছুটা পরিশ্রমও লাগে। তবে একটু চেষ্টা করলে দেখবে notation-টা সহজ ও স্বাভাবিক হয়ে গেছে। এই notation-এর একটা খটকা হলো jjkk index-এর ক্রম। তুমি হয়তো ভাবছ input neuron বোঝাতে jj আর output neuron বোঝাতে kk ব্যবহার করাই বেশি যুক্তিযুক্ত হতো — কিন্তু আসলে উল্টোটা করা হয়। এই খটকার কারণ নিচে ব্যাখ্যা করব।

Network-এর bias ও activation-এর জন্যও আমরা একই রকম notation ব্যবহার করি। স্পষ্টভাবে বললে, lthl^{\rm th} layer-এর jthj^{\rm th} neuron-এর bias বোঝাতে আমরা bjlb^l_j ব্যবহার করি। আর lthl^{\rm th} layer-এর jthj^{\rm th} neuron-এর activation বোঝাতে ব্যবহার করি ajla^l_j। নিচের diagram-এ এই notation-গুলোর ব্যবহারের উদাহরণ দেখানো হয়েছে:

bias b^l_j ও activation a^l_j-এর notation
Bias bjlb^l_j ও activation ajla^l_j-এর notation।

এই notation দিয়ে lthl^{\rm th} layer-এর jthj^{\rm th} neuron-এর activation ajla^{l}_j এবং (l1)th(l-1)^{\rm th} layer-এর activation-গুলোর সম্পর্ক নিচের equation দিয়ে প্রকাশ করা যায় (গত অধ্যায়ের Equation (4) 11+exp(jwjxjb)\frac{1}{1+\exp(-\sum_j w_j x_j-b)} ও তার আশপাশের আলোচনার সাথে তুলনা করো):

ajl=σ(kwjklakl1+bjl),(23)a^{l}_j = \sigma\left( \sum_k w^{l}_{jk} a^{l-1}_k + b^l_j \right),\tag{23}

যেখানে যোগফলটি (l1)th(l-1)^{\rm th} layer-এর সব neuron kk-এর উপর নেওয়া। এই রাশিটাকে matrix আকারে আবার লিখতে আমরা প্রতিটি layer ll-এর জন্য একটা weight matrix wlw^l সংজ্ঞায়িত করি। Weight matrix wlw^l-এর entry-গুলো হলো lthl^{\rm th} layer-এর neuron-গুলোর সাথে যুক্ত weight, অর্থাৎ jthj^{\rm th} সারি ও kthk^{\rm th} কলামের entry হলো wjklw^l_{jk}। একইভাবে, প্রতিটি layer ll-এর জন্য আমরা একটা bias vector blb^l সংজ্ঞায়িত করি। এটা কীভাবে কাজ করে তুমি হয়তো অনুমান করতে পারছ — bias vector-এর component-গুলো হলো bjlb^l_j মানগুলো, lthl^{\rm th} layer-এর প্রতিটি neuron-এর জন্য একটি করে component। আর সবশেষে আমরা একটা activation vector ala^l সংজ্ঞায়িত করি যার component-গুলো হলো activation ajla^l_j

(23)-কে matrix আকারে আবার লিখতে আমাদের শেষ যে উপাদানটা দরকার তা হলো σ\sigma-এর মতো একটা function-কে vectorize করার ধারণা। গত অধ্যায়ে আমরা vectorization সংক্ষেপে দেখেছিলাম, তবে সংক্ষেপে আবার বলি — ধারণাটা হলো আমরা একটা vector vv-এর প্রতিটি element-এ σ\sigma-এর মতো একটা function প্রয়োগ করতে চাই। এই ধরনের element-ভিত্তিক প্রয়োগ বোঝাতে আমরা স্পষ্ট notation σ(v)\sigma(v) ব্যবহার করি। অর্থাৎ σ(v)\sigma(v)-এর component-গুলো হলো σ(v)j=σ(vj)\sigma(v)_j = \sigma(v_j)। উদাহরণস্বরূপ, function f(x)=x2f(x) = x^2 হলে ff-এর vectorized রূপের প্রভাব হলো

f([23])=[f(2)f(3)]=[49],(24)f\left(\left[ \begin{array}{c} 2 \\ 3 \end{array} \right] \right) = \left[ \begin{array}{c} f(2) \\ f(3) \end{array} \right] = \left[ \begin{array}{c} 4 \\ 9 \end{array} \right],\tag{24}

অর্থাৎ vectorized ff কেবল vector-এর প্রতিটি element-কে বর্গ করে।

এসব notation মাথায় রেখে Equation (23)-কে নিচের সুন্দর ও সংক্ষিপ্ত vectorized রূপে আবার লেখা যায়

al=σ(wlal1+bl).(25)a^{l} = \sigma(w^l a^{l-1}+b^l).\tag{25}

এই রাশি এক layer-এর activation কীভাবে আগের layer-এর activation-এর সাথে সম্পর্কিত তা ভাবার একটা অনেক বেশি সামগ্রিক উপায় দেয়: আমরা কেবল activation-গুলোতে weight matrix প্রয়োগ করি, তারপর bias vector যোগ করি, আর সবশেষে σ\sigma function প্রয়োগ করি*। এই সামগ্রিক দৃষ্টিভঙ্গি প্রায়ই এতক্ষণ নেওয়া neuron-ভিত্তিক দৃষ্টিভঙ্গির চেয়ে সহজ ও সংক্ষিপ্ত (এবং তাতে কম index লাগে!)। একে index-নরক থেকে পালানোর একটা উপায় হিসেবে ভাবতে পারো, যেখানে কী ঘটছে তা নিয়ে নিখুঁত থাকা যায়। রাশিটা বাস্তবেও কাজের, কারণ বেশিরভাগ matrix library matrix গুণ, vector যোগ ও vectorization বাস্তবায়নের দ্রুত উপায় দেয়। আসলে গত অধ্যায়ের code এই রাশিটিই অন্তর্নিহিতভাবে ব্যবহার করেছিল network-এর আচরণ হিসাব করতে।

Equation (25) দিয়ে ala^l হিসাব করার সময় আমরা পথে একটা মধ্যবর্তী রাশি zlwlal1+blz^l \equiv w^l a^{l-1}+b^l হিসাব করি। এই রাশিটা যথেষ্ট কাজের, তাই একে নাম দেওয়া সার্থক: আমরা zlz^l-কে layer ll-এর neuron-গুলোর weighted input বলি। অধ্যায়ের পরে weighted input zlz^l আমরা প্রচুর ব্যবহার করব। Equation (25)-কে কখনো কখনো weighted input দিয়েও লেখা হয়, যেমন al=σ(zl)a^l = \sigma(z^l)। আরও লক্ষণীয় যে zlz^l-এর component হলো zjl=kwjklakl1+bjlz^l_j = \sum_k w^l_{jk} a^{l-1}_k+b^l_j, অর্থাৎ zjlz^l_j হলো layer ll-এর neuron jj-এর activation function-এর weighted input।

Cost function সম্পর্কে আমাদের যে দুটি অনুমান দরকার

Backpropagation-এর লক্ষ্য হলো network-এর যেকোনো weight ww বা bias bb সাপেক্ষে cost function CC-এর partial derivative C/w\partial C / \partial wC/b\partial C / \partial b হিসাব করা। Backpropagation কাজ করার জন্য cost function-এর রূপ সম্পর্কে আমাদের দুটি মূল অনুমান করতে হয়। তবে সেই অনুমানগুলো বলার আগে মাথায় একটা উদাহরণ cost function রাখা ভালো। আমরা গত অধ্যায়ের quadratic cost function ব্যবহার করব (তুলনা করো Equation (6) C(w,b)12nxy(x)a2C(w,b) \equiv \frac{1}{2n} \sum_x \| y(x) - a\|^2)। আগের অংশের notation-এ quadratic cost-এর রূপ হলো

C=12nxy(x)aL(x)2,(26)C = \frac{1}{2n} \sum_x \|y(x)-a^L(x)\|^2,\tag{26}

যেখানে: nn হলো training example-এর মোট সংখ্যা; যোগফলটি পৃথক পৃথক training example xx-এর উপর নেওয়া; y=y(x)y = y(x) হলো সংশ্লিষ্ট কাঙ্ক্ষিত output; LL হলো network-এর layer সংখ্যা; এবং aL=aL(x)a^L = a^L(x) হলো input হিসেবে xx দিলে network থেকে output হওয়া activation-গুলোর vector।

ঠিক আছে, তাহলে backpropagation প্রয়োগ করতে পারার জন্য আমাদের cost function CC সম্পর্কে কী কী অনুমান করতে হয়? প্রথম অনুমানটি হলো — cost function-কে পৃথক পৃথক training example xx-এর cost function CxC_x-এর গড় C=1nxCxC = \frac{1}{n} \sum_x C_x হিসেবে লেখা যায়। Quadratic cost function-এর ক্ষেত্রে এটা সত্যি, যেখানে একটিমাত্র training example-এর cost হলো Cx=12yaL2C_x = \frac{1}{2} \|y-a^L \|^2। এই অনুমানটি এই বইয়ে আমরা যত cost function দেখব তাদের সবার ক্ষেত্রেই সত্যি হবে।

এই অনুমান কেন দরকার তার কারণ — backpropagation আসলে আমাদের একটিমাত্র training example-এর জন্য partial derivative Cx/w\partial C_x / \partial wCx/b\partial C_x / \partial b হিসাব করতে দেয়। তারপর training example-গুলোর উপর গড় নিয়ে আমরা C/w\partial C / \partial wC/b\partial C / \partial b পুনরুদ্ধার করি। আসলে এই অনুমান মাথায় রেখে আমরা ধরে নেব training example xx স্থির করা হয়েছে, এবং xx subscript বাদ দিয়ে cost CxC_x-কে শুধু CC লিখব। শেষে আমরা xx ফিরিয়ে আনব, কিন্তু আপাতত এটা একটা notation-গত ঝামেলা যা অন্তর্নিহিত রাখাই ভালো।

Cost সম্পর্কে দ্বিতীয় অনুমানটি হলো — একে neural network-এর output-গুলোর একটা function হিসেবে লেখা যায়:

cost-কে output activation-এর function হিসেবে দেখানো diagram
Cost CC-কে output activation-গুলোর একটি function হিসেবে দেখা।

উদাহরণস্বরূপ, quadratic cost function এই শর্ত পূরণ করে, কারণ একটিমাত্র training example xx-এর quadratic cost-কে লেখা যায়

C=12yaL2=12j(yjajL)2,(27)C = \frac{1}{2} \|y-a^L\|^2 = \frac{1}{2} \sum_j (y_j-a^L_j)^2,\tag{27}

এবং তাই এটা output activation-গুলোর একটা function। অবশ্যই এই cost function কাঙ্ক্ষিত output yy-এর উপরও নির্ভর করে, আর তুমি হয়তো ভাবছ আমরা cost-কে yy-এরও function হিসেবে কেন ধরছি না। মনে রেখো, input training example xx স্থির, তাই output yy-ও একটা স্থির parameter। বিশেষত, এটা এমন কিছু নয় যাকে আমরা weight ও bias বদলে যেকোনোভাবে পরিবর্তন করতে পারি, অর্থাৎ এটা এমন কিছু নয় যা neural network শেখে। তাই CC-কে কেবল output activation aLa^L-এর একটা function হিসেবে ধরা যুক্তিযুক্ত, যেখানে yy কেবল একটা parameter যা সেই function সংজ্ঞায়িত করতে সাহায্য করে।

Hadamard product, sts \odot t

Backpropagation algorithm প্রচলিত কিছু linear algebra operation-এর উপর ভিত্তি করে তৈরি — যেমন vector যোগ করা, একটা vector-কে matrix দিয়ে গুণ করা ইত্যাদি। তবে এর একটা operation একটু কম-প্রচলিত। বিশেষভাবে, ধরো sstt হলো একই dimension-এর দুটি vector। তখন আমরা sts \odot t দিয়ে এই দুই vector-এর element-ভিত্তিক গুণফল বোঝাই। অর্থাৎ sts \odot t-এর component-গুলো হলো (st)j=sjtj(s \odot t)_j = s_j t_j। উদাহরণস্বরূপ,

[12][34]=[1324]=[38].(28)\left[\begin{array}{c} 1 \\ 2 \end{array}\right] \odot \left[\begin{array}{c} 3 \\ 4\end{array} \right] = \left[ \begin{array}{c} 1 * 3 \\ 2 * 4 \end{array} \right] = \left[ \begin{array}{c} 3 \\ 8 \end{array} \right].\tag{28}

এই ধরনের element-ভিত্তিক গুণকে কখনো কখনো Hadamard product বা Schur product বলা হয়। আমরা একে Hadamard product বলব। ভালো matrix library সাধারণত Hadamard product-এর দ্রুত বাস্তবায়ন দেয়, আর backpropagation বাস্তবায়নের সময় সেটা কাজে লাগে।

Backpropagation-এর পেছনের চারটি মৌলিক equation

Backpropagation আসলে এটা বোঝা যে — network-এর weight ও bias বদলালে cost function কীভাবে বদলায়। শেষ বিচারে এর মানে partial derivative C/wjkl\partial C / \partial w^l_{jk}C/bjl\partial C / \partial b^l_j হিসাব করা। কিন্তু সেগুলো হিসাব করতে আমরা প্রথমে একটা মধ্যবর্তী রাশি δjl\delta^l_j চালু করি, যাকে আমরা lthl^{\rm th} layer-এর jthj^{\rm th} neuron-এর error বলি। Backpropagation আমাদের error δjl\delta^l_j হিসাব করার একটা পদ্ধতি দেবে, এবং তারপর δjl\delta^l_j-কে C/wjkl\partial C / \partial w^l_{jk}C/bjl\partial C / \partial b^l_j-এর সাথে সম্পর্কিত করবে।

Error কীভাবে সংজ্ঞায়িত হয় বুঝতে, কল্পনা করো আমাদের neural network-এ একটা দৈত্য (demon) আছে:

neuron-এর weighted input-এ পরিবর্তন আনা একটি দৈত্য
Network-এর একটি neuron-এ বসে থাকা দৈত্য।

দৈত্যটি ll layer-এর jthj^{\rm th} neuron-এ বসে আছে। Neuron-এ input এলে দৈত্যটি neuron-এর কাজে গোলযোগ বাধায়। সে neuron-এর weighted input-এ একটা সামান্য পরিবর্তন Δzjl\Delta z^l_j যোগ করে, যাতে σ(zjl)\sigma(z^l_j) output দেওয়ার বদলে neuron-টি output দেয় σ(zjl+Δzjl)\sigma(z^l_j+\Delta z^l_j)। এই পরিবর্তন network-এর পরের layer-গুলোর মধ্য দিয়ে ছড়িয়ে পড়ে, শেষে সামগ্রিক cost-কে CzjlΔzjl\frac{\partial C}{\partial z^l_j} \Delta z^l_j পরিমাণ বদলে দেয়।

এখন এই দৈত্যটি একটা ভালো দৈত্য, আর সে তোমাকে cost কমাতে সাহায্য করতে চাইছে, অর্থাৎ এমন একটা Δzjl\Delta z^l_j খুঁজছে যা cost ছোট করে। ধরো Czjl\frac{\partial C}{\partial z^l_j}-এর মান বড় (ধনাত্মক বা ঋণাত্মক যেকোনোটা)। তখন দৈত্যটি Czjl\frac{\partial C}{\partial z^l_j}-এর বিপরীত চিহ্নের Δzjl\Delta z^l_j বেছে নিয়ে cost বেশ খানিকটা কমাতে পারে। উল্টোদিকে, Czjl\frac{\partial C}{\partial z^l_j} শূন্যের কাছাকাছি হলে দৈত্যটি weighted input zjlz^l_j বিচলিত করে cost খুব একটা উন্নত করতে পারে না। দৈত্যটি যতটা বুঝতে পারে, neuron-টি ইতিমধ্যে মোটামুটি optimal-এর কাছাকাছি*। তাই একটা heuristic অর্থে Czjl\frac{\partial C}{\partial z^l_j} হলো neuron-টির error-এর একটা পরিমাপ।

এই গল্পে অনুপ্রাণিত হয়ে আমরা ll layer-এর neuron jj-এর error δjl\delta^l_j সংজ্ঞায়িত করি এভাবে

δjlCzjl.(29)\delta^l_j \equiv \frac{\partial C}{\partial z^l_j}.\tag{29}

আমাদের সাধারণ প্রথা অনুযায়ী, ll layer-এর সাথে যুক্ত error-গুলোর vector বোঝাতে আমরা δl\delta^l ব্যবহার করি। Backpropagation আমাদের প্রতিটি layer-এর জন্য δl\delta^l হিসাব করার একটা উপায় দেবে, তারপর সেই error-গুলোকে আমাদের আসল আগ্রহের রাশি C/wjkl\partial C / \partial w^l_{jk}C/bjl\partial C / \partial b^l_j-এর সাথে সম্পর্কিত করবে।

তুমি হয়তো ভাবছ দৈত্যটি weighted input zjlz^l_j কেন বদলাচ্ছে। দৈত্যটি output activation ajla^l_j বদলাচ্ছে ভাবা নিশ্চয়ই বেশি স্বাভাবিক হতো, ফলে error-এর পরিমাপ হিসেবে আমরা Cajl\frac{\partial C}{\partial a^l_j} ব্যবহার করতাম। আসলে তুমি যদি তা করো তবে নিচের আলোচনার সাথে অনেকটা একই রকম ফল পাবে। কিন্তু এতে backpropagation-এর উপস্থাপনা algebraically একটু বেশি জটিল হয়ে যায়। তাই আমরা error-এর পরিমাপ হিসেবে δjl=Czjl\delta^l_j = \frac{\partial C}{\partial z^l_j}-এই থাকব*

আক্রমণের পরিকল্পনা: Backpropagation চারটি মৌলিক equation ঘিরে গড়ে উঠেছে। একসাথে এই equation-গুলো আমাদের error δl\delta^l এবং cost function-এর gradient — দুটোই হিসাব করার উপায় দেয়। নিচে আমি চারটি equation দিচ্ছি। তবে সাবধান করছি: equation চারটি তাৎক্ষণিকভাবে আত্মস্থ হয়ে যাবে এমন আশা কোরো না। এমন আশা হতাশায় পরিণত হবে। আসলে backpropagation equation-গুলো এত সমৃদ্ধ যে এগুলো ভালোভাবে বুঝতে যথেষ্ট সময় ও ধৈর্য লাগে, যত তুমি ক্রমশ এগুলোর গভীরে ঢুকবে। সুখবর হলো, এই ধৈর্যের পুরস্কার বহুগুণে ফিরে আসে। তাই এই অংশের আলোচনা কেবল একটা শুরু, যা equation-গুলো পুঙ্খানুপুঙ্খভাবে বোঝার পথে তোমাকে সাহায্য করবে।

অধ্যায়ের পরে কীভাবে আমরা equation-গুলোর আরও গভীরে যাব তার একটা পূর্বাভাস: আমি equation-গুলোর একটা সংক্ষিপ্ত প্রমাণ দেব, যা ব্যাখ্যা করতে সাহায্য করবে কেন এগুলো সত্যি; আমরা equation-গুলোকে pseudocode আকারে algorithm হিসেবে আবার লিখব, এবং দেখব সেই pseudocode কীভাবে বাস্তব, চলমান Python code-এ বাস্তবায়িত হতে পারে; আর অধ্যায়ের শেষ অংশে আমরা একটা স্বজ্ঞাত ছবি গড়ে তুলব — backpropagation equation-গুলোর মানে কী, এবং কেউ কীভাবে শূন্য থেকে এগুলো আবিষ্কার করতে পারত। পথে আমরা বারবার এই চারটি মৌলিক equation-এ ফিরে আসব, আর তোমার বোঝাপড়া যত গভীর হবে, equation-গুলো তত স্বচ্ছন্দ — এমনকি হয়তো সুন্দর ও স্বাভাবিক — মনে হতে থাকবে।

Output layer-এর error δL\delta^L-এর equation: δL\delta^L-এর component-গুলো দেওয়া হয়

δjL=CajLσ(zjL).(BP1)\delta^L_j = \frac{\partial C}{\partial a^L_j} \sigma'(z^L_j).\tag{BP1}

এটা খুবই স্বাভাবিক একটা রাশি। ডানদিকের প্রথম পদ C/ajL\partial C / \partial a^L_j কেবল মাপে jthj^{\rm th} output activation-এর function হিসেবে cost কত দ্রুত বদলাচ্ছে। যেমন, CC যদি কোনো একটা নির্দিষ্ট output neuron jj-এর উপর খুব একটা নির্ভর না করে, তবে δjL\delta^L_j ছোট হবে, যা আমরা আশাই করি। ডানদিকের দ্বিতীয় পদ σ(zjL)\sigma'(z^L_j) মাপে activation function σ\sigma কত দ্রুত zjLz^L_j-তে বদলাচ্ছে।

লক্ষ করো (BP1)-এর সবকিছুই সহজে হিসাব করা যায়। বিশেষত, network-এর আচরণ হিসাব করার সময়ই আমরা zjLz^L_j হিসাব করি, আর σ(zjL)\sigma'(z^L_j) হিসাব করতে কেবল সামান্য বাড়তি খরচ লাগে।C/ajL\partial C / \partial a^L_j-এর নিখুঁত রূপ অবশ্যই cost function-এর রূপের উপর নির্ভর করবে। তবে cost function জানা থাকলে C/ajL\partial C / \partial a^L_j হিসাব করতে খুব একটা ঝামেলা হওয়ার কথা নয়। যেমন, quadratic cost function ব্যবহার করলে C=12j(yjajL)2C = \frac{1}{2} \sum_j (y_j-a^L_j)^2, আর তাই C/ajL=(ajLyj)\partial C / \partial a^L_j = (a_j^L-y_j), যা স্পষ্টতই সহজে হিসাব করা যায়।

Equation (BP1) হলো δL\delta^L-এর একটা component-ভিত্তিক রাশি। এটা পুরোপুরি ভালো একটা রাশি, কিন্তু backpropagation-এর জন্য আমরা যে matrix-ভিত্তিক রূপ চাই তা নয়। তবে equation-টিকে matrix-ভিত্তিক রূপে আবার লেখা সহজ, যেমন

δL=aCσ(zL).(BP1a)\delta^L = \nabla_a C \odot \sigma'(z^L).\tag{BP1a}

এখানে aC\nabla_a C-কে এমন একটা vector হিসেবে সংজ্ঞায়িত করা হয় যার component-গুলো হলো partial derivative C/ajL\partial C / \partial a^L_jaC\nabla_a C-কে তুমি output activation সাপেক্ষে CC-এর পরিবর্তনের হার প্রকাশকারী হিসেবে ভাবতে পারো। সহজেই দেখা যায় Equation (BP1a) ও (BP1) সমতুল্য, আর সেই কারণে এখন থেকে আমরা দুটো equation-কেই বোঝাতে (BP1) পরস্পর-বিনিময়যোগ্যভাবে ব্যবহার করব। উদাহরণস্বরূপ, quadratic cost-এর ক্ষেত্রে আমরা পাই aC=(aLy)\nabla_a C = (a^L-y), আর তাই (BP1)-এর পুরোপুরি matrix-ভিত্তিক রূপ হয়

δL=(aLy)σ(zL).(30)\delta^L = (a^L-y) \odot \sigma'(z^L).\tag{30}

দেখতেই পাচ্ছ, এই রাশির সবকিছুরই একটা সুন্দর vector রূপ আছে, এবং Numpy-র মতো library দিয়ে সহজেই হিসাব করা যায়।

পরের layer-এর error δl+1\delta^{l+1}-এর সাপেক্ষে error δl\delta^l-এর equation: বিশেষভাবে

δl=((wl+1)Tδl+1)σ(zl),(BP2)\delta^l = ((w^{l+1})^T \delta^{l+1}) \odot \sigma'(z^l),\tag{BP2}

যেখানে (wl+1)T(w^{l+1})^T হলো (l+1)th(l+1)^{\rm th} layer-এর weight matrix wl+1w^{l+1}-এর transpose। এই equation জটিল দেখায়, কিন্তু এর প্রতিটি উপাদানের একটা সুন্দর ব্যাখ্যা আছে। ধরো আমরা l+1thl+1^{\rm th} layer-এর error δl+1\delta^{l+1} জানি। যখন আমরা transpose weight matrix (wl+1)T(w^{l+1})^T প্রয়োগ করি, তখন একে আমরা স্বজ্ঞাতভাবে network-এর মধ্য দিয়ে error-কে পেছনের দিকে সরানো হিসেবে ভাবতে পারি, যা আমাদের lthl^{\rm th} layer-এর output-এ error-এর একটা মাপ দেয়। তারপর আমরা Hadamard product σ(zl)\odot \sigma'(z^l) নিই। এটা error-কে layer ll-এর activation function-এর মধ্য দিয়ে পেছনে সরায়, যা আমাদের layer ll-এর weighted input-এ error δl\delta^l দেয়।

(BP2)-কে (BP1)-এর সাথে মিলিয়ে আমরা network-এর যেকোনো layer-এর জন্য error δl\delta^l হিসাব করতে পারি। আমরা শুরু করি (BP1) দিয়ে δL\delta^L হিসাব করে, তারপর Equation (BP2) প্রয়োগ করে δL1\delta^{L-1} হিসাব করি, তারপর আবার Equation (BP2) দিয়ে δL2\delta^{L-2} হিসাব করি, এভাবে network-এর পুরো পেছন পর্যন্ত যাই।

Network-এর যেকোনো bias সাপেক্ষে cost-এর পরিবর্তনের হারের equation: বিশেষভাবে:

Cbjl=δjl.(BP3)\frac{\partial C}{\partial b^l_j} = \delta^l_j.\tag{BP3}

অর্থাৎ error δjl\delta^l_j ঠিক পরিবর্তনের হার C/bjl\partial C / \partial b^l_j-এর সমান। এটা দারুণ খবর, কারণ (BP1) ও (BP2) আমাদের ইতিমধ্যেই বলে দিয়েছে কীভাবে δjl\delta^l_j হিসাব করতে হয়। আমরা (BP3)-কে সংক্ষেপে লিখতে পারি

Cb=δ,(31)\frac{\partial C}{\partial b} = \delta,\tag{31}

যেখানে বোঝা যাচ্ছে δ\delta-কে bias bb-এর সাথে একই neuron-এ মূল্যায়ন করা হচ্ছে।

Network-এর যেকোনো weight সাপেক্ষে cost-এর পরিবর্তনের হারের equation: বিশেষভাবে:

Cwjkl=akl1δjl.(BP4)\frac{\partial C}{\partial w^l_{jk}} = a^{l-1}_k \delta^l_j.\tag{BP4}

এটা আমাদের বলে দেয় কীভাবে partial derivative C/wjkl\partial C / \partial w^l_{jk}-কে আমরা ইতিমধ্যেই হিসাব করতে জানা রাশি δl\delta^lal1a^{l-1}-এর সাপেক্ষে হিসাব করতে হয়। Equation-টিকে কম index-ভারী notation-এ আবার লেখা যায়

Cw=ainδout,(32)\frac{\partial C}{\partial w} = a_{\rm in} \delta_{\rm out},\tag{32}

যেখানে বোঝা যাচ্ছে aina_{\rm in} হলো weight ww-এ input দেওয়া neuron-এর activation, আর δout\delta_{\rm out} হলো weight ww থেকে output নেওয়া neuron-এর error। কেবল weight ww এবং সেই weight দিয়ে যুক্ত দুটি neuron-এর দিকে নিবিড়ভাবে তাকালে আমরা একে এভাবে দেখাতে পারি:

একটি weight ও তার সাথে যুক্ত দুটি neuron
Weight ww, এর input activation aina_{\rm in} ও output error δout\delta_{\rm out}

Equation (32)-এর একটা সুন্দর পরিণতি হলো — activation aina_{\rm in} ছোট হলে, অর্থাৎ ain0a_{\rm in} \approx 0 হলে, gradient পদ C/w\partial C / \partial w-ও ছোট হওয়ার প্রবণতা দেখায়। এই ক্ষেত্রে আমরা বলব weight-টি ধীরে শেখে, অর্থাৎ gradient descent-এর সময় এটা খুব একটা বদলায় না। অন্যভাবে বললে, (BP4)-এর একটা পরিণতি হলো low-activation neuron থেকে বেরিয়ে আসা weight-গুলো ধীরে শেখে।

(BP1)–(BP4) থেকে এই ধরনের আরও কিছু অন্তর্দৃষ্টি পাওয়া যায়। চলো output layer দিয়ে শুরু করি। (BP1)-এর σ(zjL)\sigma'(z^L_j) পদটি বিবেচনা করো। গত অধ্যায়ের sigmoid function-এর graph থেকে মনে করো — σ(zjL)\sigma(z^L_j) যখন প্রায় 00 বা 11 তখন σ\sigma function খুব সমতল (flat) হয়ে যায়। এমন হলে আমরা পাই σ(zjL)0\sigma'(z^L_j) \approx 0। তাই শিক্ষা হলো — শেষ layer-এর একটা weight ধীরে শিখবে যদি output neuron হয় low activation (0\approx 0) বা high activation (1\approx 1)। এই ক্ষেত্রে সাধারণভাবে বলা হয় output neuron-টি saturated হয়ে গেছে, ফলে weight-টি শেখা বন্ধ করে দিয়েছে (বা ধীরে শিখছে)। Output neuron-এর bias-এর ক্ষেত্রেও একই কথা খাটে।

আগের layer-গুলোর জন্যও আমরা একই রকম অন্তর্দৃষ্টি পেতে পারি। বিশেষত, (BP2)-এর σ(zl)\sigma'(z^l) পদটি লক্ষ করো। এর মানে neuron যদি saturation-এর কাছাকাছি থাকে তবে δjl\delta^l_j ছোট হওয়ার সম্ভাবনা থাকে। আর এর ফলে, একটা saturated neuron-এ input দেওয়া যেকোনো weight ধীরে শিখবে*

সব মিলিয়ে আমরা শিখলাম — একটা weight ধীরে শিখবে যদি হয় input neuron low-activation হয়, নয়তো output neuron saturated হয়, অর্থাৎ হয় high- বা low-activation হয়।

এই পর্যবেক্ষণগুলোর কোনোটাই খুব একটা অবাক করার মতো নয়। তবু এগুলো একটা neural network শেখার সময় কী ঘটছে সে সম্পর্কে আমাদের মানসিক model উন্নত করতে সাহায্য করে। আরও, এই ধরনের যুক্তি আমরা উল্টোভাবেও কাজে লাগাতে পারি। দেখা যায় এই চারটি মৌলিক equation যেকোনো activation function-এর জন্যই খাটে, কেবল standard sigmoid function নয় (কারণ, একটু পরেই দেখব, প্রমাণে σ\sigma-এর কোনো বিশেষ ধর্ম ব্যবহার করা হয় না)। তাই আমরা এই equation-গুলো ব্যবহার করে এমন activation function নকশা করতে পারি যাদের নির্দিষ্ট কাঙ্ক্ষিত শেখার ধর্ম আছে। ধারণা দিতে একটা উদাহরণ দিই — ধরো আমরা এমন একটা (non-sigmoid) activation function σ\sigma বেছে নিলাম যাতে σ\sigma' সবসময় ধনাত্মক, আর কখনো শূন্যের কাছাকাছি যায় না। তাহলে সাধারণ sigmoid neuron saturate হলে যে শেখার ধীরগতি ঘটে তা এড়ানো যেত। বইয়ের পরে আমরা এমন উদাহরণ দেখব যেখানে activation function-এ এই ধরনের পরিবর্তন করা হয়। চারটি equation (BP1)–(BP4) মাথায় রাখলে এমন পরিবর্তন কেন চেষ্টা করা হয় এবং তার প্রভাব কী হতে পারে তা ব্যাখ্যা করতে সাহায্য হয়।

চারটি মৌলিক backpropagation equation সারাংশ
Backpropagation-এর চারটি মৌলিক equation।

চারটি মৌলিক equation-এর প্রমাণ (ঐচ্ছিক)

এবার আমরা চারটি মৌলিক equation (BP1)–(BP4) প্রমাণ করব। চারটিই multivariable calculus-এর chain rule-এর পরিণতি। তুমি যদি chain rule-এ স্বচ্ছন্দ হও, তবে আমি জোর দিয়ে উৎসাহ দিচ্ছি — পড়ার আগে নিজেই derivation চেষ্টা করো।

শুরু করি Equation (BP1) দিয়ে, যা output error δL\delta^L-এর একটা রাশি দেয়। এটা প্রমাণ করতে মনে করো সংজ্ঞা অনুযায়ী

δjL=CzjL.(36)\delta^L_j = \frac{\partial C}{\partial z^L_j}.\tag{36}

Chain rule প্রয়োগ করে, উপরের partial derivative-কে আমরা output activation সাপেক্ষে partial derivative দিয়ে আবার প্রকাশ করতে পারি,

δjL=kCakLakLzjL,(37)\delta^L_j = \sum_k \frac{\partial C}{\partial a^L_k} \frac{\partial a^L_k}{\partial z^L_j},\tag{37}

যেখানে যোগফলটি output layer-এর সব neuron kk-এর উপর নেওয়া। অবশ্যই, kthk^{\rm th} neuron-এর output activation akLa^L_k কেবল jthj^{\rm th} neuron-এর weighted input zjLz^L_j-এর উপর নির্ভর করে যখন k=jk = j। আর তাই akL/zjL\partial a^L_k / \partial z^L_j বিলীন হয়ে যায় যখন kjk \neq j। ফলে আগের equation-টিকে আমরা সরল করতে পারি

δjL=CajLajLzjL.(38)\delta^L_j = \frac{\partial C}{\partial a^L_j} \frac{\partial a^L_j}{\partial z^L_j}.\tag{38}

মনে করে দেখো ajL=σ(zjL)a^L_j = \sigma(z^L_j), তাই ডানদিকের দ্বিতীয় পদটিকে σ(zjL)\sigma'(z^L_j) লেখা যায়, আর equation-টি হয়

δjL=CajLσ(zjL),(39)\delta^L_j = \frac{\partial C}{\partial a^L_j} \sigma'(z^L_j),\tag{39}

যা component আকারে ঠিক (BP1)।

এরপর আমরা (BP2) প্রমাণ করব, যা পরের layer-এর error δl+1\delta^{l+1}-এর সাপেক্ষে error δl\delta^l-এর একটা equation দেয়। এটা করতে আমরা δjl=C/zjl\delta^l_j = \partial C / \partial z^l_j-কে δkl+1=C/zkl+1\delta^{l+1}_k = \partial C / \partial z^{l+1}_k-এর সাপেক্ষে আবার লিখতে চাই। এটা আমরা chain rule দিয়ে করতে পারি,

δjl=Czjl(40)\delta^l_j = \frac{\partial C}{\partial z^l_j}\tag{40}
=kCzkl+1zkl+1zjl(41)= \sum_k \frac{\partial C}{\partial z^{l+1}_k} \frac{\partial z^{l+1}_k}{\partial z^l_j}\tag{41}
=kzkl+1zjlδkl+1,(42)= \sum_k \frac{\partial z^{l+1}_k}{\partial z^l_j} \delta^{l+1}_k,\tag{42}

যেখানে শেষ লাইনে আমরা ডানদিকের দুটি পদ অদলবদল করেছি, এবং δkl+1\delta^{l+1}_k-এর সংজ্ঞা বসিয়েছি। শেষ লাইনের প্রথম পদটি মূল্যায়ন করতে লক্ষ করো যে

zkl+1=jwkjl+1ajl+bkl+1=jwkjl+1σ(zjl)+bkl+1.(43)z^{l+1}_k = \sum_j w^{l+1}_{kj} a^l_j +b^{l+1}_k = \sum_j w^{l+1}_{kj} \sigma(z^l_j) +b^{l+1}_k.\tag{43}

Differentiate করে আমরা পাই

zkl+1zjl=wkjl+1σ(zjl).(44)\frac{\partial z^{l+1}_k}{\partial z^l_j} = w^{l+1}_{kj} \sigma'(z^l_j).\tag{44}

(42)-তে আবার বসিয়ে আমরা পাই

δjl=kwkjl+1δkl+1σ(zjl).(45)\delta^l_j = \sum_k w^{l+1}_{kj} \delta^{l+1}_k \sigma'(z^l_j).\tag{45}

যা component আকারে লেখা ঠিক (BP2)।

শেষ যে দুটি equation আমরা প্রমাণ করতে চাই তা হলো (BP3) ও (BP4)। এগুলোও chain rule থেকে আসে, উপরের দুটি equation-এর প্রমাণের মতোই। এগুলো আমি তোমার জন্য একটা অনুশীলনী হিসেবে রেখে দিলাম।

এতে backpropagation-এর চারটি মৌলিক equation-এর প্রমাণ সম্পূর্ণ হলো। প্রমাণটা জটিল মনে হতে পারে। কিন্তু এটা আসলে কেবল যত্ন করে chain rule প্রয়োগ করার ফল মাত্র। একটু কম সংক্ষিপ্তভাবে বললে, backpropagation-কে আমরা multivariable calculus-এর chain rule নিয়মিতভাবে প্রয়োগ করে cost function-এর gradient হিসাব করার একটা উপায় হিসেবে ভাবতে পারি। Backpropagation আসলে এটুকুই — বাকিটা খুঁটিনাটি।

Backpropagation algorithm

Backpropagation equation-গুলো আমাদের cost function-এর gradient হিসাব করার একটা উপায় দেয়। চলো এটা স্পষ্টভাবে একটা algorithm আকারে লিখে ফেলি:

  1. Input xx: input layer-এর জন্য সংশ্লিষ্ট activation a1a^{1} সেট করো।
  2. Feedforward: প্রতিটি l=2,3,,Ll = 2, 3, \ldots, L-এর জন্য zl=wlal1+blz^{l} = w^l a^{l-1}+b^l এবং al=σ(zl)a^{l} = \sigma(z^{l}) হিসাব করো।
  3. Output error δL\delta^L: vector δL=aCσ(zL)\delta^{L} = \nabla_a C \odot \sigma'(z^L) হিসাব করো।
  4. Error backpropagate করো: প্রতিটি l=L1,L2,,2l = L-1, L-2, \ldots, 2-এর জন্য δl=((wl+1)Tδl+1)σ(zl)\delta^{l} = ((w^{l+1})^T \delta^{l+1}) \odot \sigma'(z^{l}) হিসাব করো।
  5. Output: cost function-এর gradient দেওয়া হয় Cwjkl=akl1δjl\frac{\partial C}{\partial w^l_{jk}} = a^{l-1}_k \delta^l_jCbjl=δjl\frac{\partial C}{\partial b^l_j} = \delta^l_j দিয়ে।

Algorithm-টা পরীক্ষা করলে বুঝবে কেন একে backpropagation বলা হয়। আমরা error vector δl\delta^l পেছন থেকে — শেষ layer থেকে শুরু করে — পেছনের দিকে হিসাব করি। Network-এর মধ্য দিয়ে পেছন দিকে যাওয়াটা অদ্ভুত মনে হতে পারে। কিন্তু backpropagation-এর প্রমাণের কথা ভাবলে দেখবে এই পেছনদিকে চলাটা আসলে এই সত্যেরই পরিণতি যে cost হলো network-এর output-গুলোর একটা function। আগের weight ও bias-এর সাথে cost কীভাবে বদলায় তা বুঝতে আমাদের বারবার chain rule প্রয়োগ করতে হয়, layer-গুলোর মধ্য দিয়ে পেছনে গিয়ে কাজের রাশি বের করতে হয়।

উপরে আমি যেভাবে বর্ণনা করলাম, backpropagation algorithm একটিমাত্র training example C=CxC = C_x-এর জন্য cost function-এর gradient হিসাব করে। বাস্তবে, backpropagation-কে stochastic gradient descent-এর মতো একটা learning algorithm-এর সাথে মিলিয়ে ব্যবহার করাই প্রচলিত, যেখানে আমরা অনেকগুলো training example-এর জন্য gradient হিসাব করি। বিশেষভাবে, mm-টি training example-এর একটা mini-batch দিলে নিচের algorithm সেই mini-batch-এর ভিত্তিতে একটা gradient descent শেখার পদক্ষেপ প্রয়োগ করে:

  1. একগুচ্ছ training example input দাও।
  2. প্রতিটি training example xx-এর জন্য: সংশ্লিষ্ট input activation ax,1a^{x,1} সেট করো, এবং নিচের পদক্ষেপগুলো সম্পন্ন করো:
    • Feedforward: প্রতিটি l=2,3,,Ll = 2, 3, \ldots, L-এর জন্য zx,l=wlax,l1+blz^{x,l} = w^l a^{x,l-1}+b^l এবং ax,l=σ(zx,l)a^{x,l} = \sigma(z^{x,l}) হিসাব করো।
    • Output error δx,L\delta^{x,L}: vector δx,L=aCxσ(zx,L)\delta^{x,L} = \nabla_a C_x \odot \sigma'(z^{x,L}) হিসাব করো।
    • Error backpropagate করো: প্রতিটি l=L1,L2,,2l = L-1, L-2, \ldots, 2-এর জন্য δx,l=((wl+1)Tδx,l+1)σ(zx,l)\delta^{x,l} = ((w^{l+1})^T \delta^{x,l+1}) \odot \sigma'(z^{x,l}) হিসাব করো।
  3. Gradient descent: প্রতিটি l=L,L1,,2l = L, L-1, \ldots, 2-এর জন্য weight-গুলো নিয়ম wlwlηmxδx,l(ax,l1)Tw^l \rightarrow w^l-\frac{\eta}{m} \sum_x \delta^{x,l} (a^{x,l-1})^T অনুযায়ী, এবং bias-গুলো নিয়ম blblηmxδx,lb^l \rightarrow b^l-\frac{\eta}{m} \sum_x \delta^{x,l} অনুযায়ী update করো।

অবশ্যই, বাস্তবে stochastic gradient descent বাস্তবায়ন করতে তোমার একটা বাইরের loop-ও লাগবে যা training example-এর mini-batch তৈরি করে, এবং আরেকটা বাইরের loop যা training-এর একাধিক epoch পেরিয়ে যায়। সরলতার জন্য আমি এগুলো বাদ দিয়েছি।

Backpropagation-এর code

বিমূর্তভাবে backpropagation বুঝে নেওয়ার পর, এবার আমরা গত অধ্যায়ে backpropagation বাস্তবায়ন করতে যে code ব্যবহার করেছিলাম তা বুঝতে পারব। সেই অধ্যায় থেকে মনে করো — code-টি ছিল Network class-এর update_mini_batchbackprop method-এ। এই method-গুলোর code হলো উপরে বর্ণিত algorithm-এর একটা সরাসরি অনুবাদ। বিশেষভাবে, update_mini_batch method চলতি mini_batch-এর training example-গুলোর জন্য gradient হিসাব করে Network-এর weight ও bias update করে:

network.pypython
class Network(object):
...
    def update_mini_batch(self, mini_batch, eta):
        """Update the network's weights and biases by applying
        gradient descent using backpropagation to a single mini batch.
        The "mini_batch" is a list of tuples "(x, y)", and "eta"
        is the learning rate."""
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        for x, y in mini_batch:
            delta_nabla_b, delta_nabla_w = self.backprop(x, y)
            nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
            nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
        self.weights = [w-(eta/len(mini_batch))*nw
                        for w, nw in zip(self.weights, nabla_w)]
        self.biases = [b-(eta/len(mini_batch))*nb
                       for b, nb in zip(self.biases, nabla_b)]

বেশিরভাগ কাজ করে এই লাইনটি — delta_nabla_b, delta_nabla_w = self.backprop(x, y) — যা backprop method ব্যবহার করে partial derivative Cx/bjl\partial C_x / \partial b^l_jCx/wjkl\partial C_x / \partial w^l_{jk} বের করে। backprop method আগের অংশের algorithm-কে নিবিড়ভাবে অনুসরণ করে। একটা ছোট্ট পরিবর্তন আছে — layer-গুলোকে index করতে আমরা একটু ভিন্ন পদ্ধতি ব্যবহার করি। এই পরিবর্তনটি করা হয়েছে Python-এর একটা বৈশিষ্ট্যের সুবিধা নিতে, অর্থাৎ list-এর শেষ থেকে পেছনে গোনার জন্য ঋণাত্মক list index ব্যবহার করার সুবিধা। যেমন, l[-3] হলো একটা list l-এর শেষ থেকে তৃতীয় entry। backprop-এর code নিচে দেওয়া হলো, সাথে কয়েকটা helper function, যেগুলো σ\sigma function, এর derivative σ\sigma', এবং cost function-এর derivative হিসাব করতে ব্যবহৃত হয়। এগুলো অন্তর্ভুক্ত থাকায় তুমি code-টা স্বয়ংসম্পূর্ণভাবে বুঝতে পারবে। কোথাও আটকে গেলে, code-এর মূল বর্ণনা (ও সম্পূর্ণ listing) দেখা সাহায্য করতে পারে।

network.pypython
class Network(object):
...
   def backprop(self, x, y):
        """Return a tuple "(nabla_b, nabla_w)" representing the
        gradient for the cost function C_x.  "nabla_b" and
        "nabla_w" are layer-by-layer lists of numpy arrays, similar
        to "self.biases" and "self.weights"."""
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        # feedforward
        activation = x
        activations = [x] # list to store all the activations, layer by layer
        zs = [] # list to store all the z vectors, layer by layer
        for b, w in zip(self.biases, self.weights):
            z = np.dot(w, activation)+b
            zs.append(z)
            activation = sigmoid(z)
            activations.append(activation)
        # backward pass
        delta = self.cost_derivative(activations[-1], y) * \
            sigmoid_prime(zs[-1])
        nabla_b[-1] = delta
        nabla_w[-1] = np.dot(delta, activations[-2].transpose())
        # Note that the variable l in the loop below is used a little
        # differently to the notation in Chapter 2 of the book.  Here,
        # l = 1 means the last layer of neurons, l = 2 is the
        # second-last layer, and so on.  It's a renumbering of the
        # scheme in the book, used here to take advantage of the fact
        # that Python can use negative indices in lists.
        for l in xrange(2, self.num_layers):
            z = zs[-l]
            sp = sigmoid_prime(z)
            delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
            nabla_b[-l] = delta
            nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())
        return (nabla_b, nabla_w)

...

    def cost_derivative(self, output_activations, y):
        """Return the vector of partial derivatives \partial C_x /
        \partial a for the output activations."""
        return (output_activations-y)

def sigmoid(z):
    """The sigmoid function."""
    return 1.0/(1.0+np.exp(-z))

def sigmoid_prime(z):
    """Derivative of the sigmoid function."""
    return sigmoid(z)*(1-sigmoid(z))

কোন অর্থে backpropagation একটি দ্রুত algorithm?

কোন অর্থে backpropagation একটা দ্রুত algorithm? এই প্রশ্নের উত্তর দিতে চলো gradient হিসাব করার আরেকটা পদ্ধতি বিবেচনা করি। কল্পনা করো এটা neural network গবেষণার একদম গোড়ার দিন। হয়তো ১৯৫০ বা ১৯৬০-এর দশক, আর তুমিই পৃথিবীর প্রথম ব্যক্তি যে শেখার জন্য gradient descent ব্যবহারের কথা ভাবলে! কিন্তু ধারণাটা কাজে লাগাতে তোমার cost function-এর gradient হিসাব করার একটা উপায় দরকার। তুমি তোমার calculus-এর জ্ঞান মনে করে দেখো, আর ঠিক করো chain rule দিয়ে gradient হিসাব করা যায় কিনা চেষ্টা করবে। কিন্তু একটু নাড়াচাড়া করার পর algebra জটিল দেখায়, আর তুমি নিরুৎসাহিত হয়ে পড়ো। তাই তুমি আরেকটা পদ্ধতি খুঁজতে যাও। তুমি ঠিক করো cost-কে কেবল weight-গুলোর একটা function C=C(w)C = C(w) হিসেবে ধরবে (bias-এর কথায় একটু পরে আসব)। তুমি weight-গুলোকে w1,w2,w_1, w_2, \ldots নম্বর দাও, আর কোনো একটা নির্দিষ্ট weight wjw_j-এর জন্য C/wj\partial C / \partial w_j হিসাব করতে চাও। এটা করার একটা সুস্পষ্ট উপায় হলো নিচের approximation ব্যবহার করা

CwjC(w+ϵej)C(w)ϵ,(46)\frac{\partial C}{\partial w_{j}} \approx \frac{C(w+\epsilon e_j)-C(w)}{\epsilon},\tag{46}

যেখানে ϵ>0\epsilon > 0 একটা ছোট ধনাত্মক সংখ্যা, আর eje_j হলো jthj^{\rm th} দিকের unit vector। অন্যভাবে বললে, wjw_j-এর দুটি সামান্য ভিন্ন মানের জন্য cost CC হিসাব করে, তারপর Equation (46) প্রয়োগ করে আমরা C/wj\partial C / \partial w_j আন্দাজ করতে পারি। একই ধারণা আমাদের bias সাপেক্ষে partial derivative C/b\partial C / \partial b হিসাব করতেও দেবে।

এই পদ্ধতিকে খুব সম্ভাবনাময় মনে হয়। ধারণাগতভাবে এটা সরল, আর কয়েক লাইন code দিয়েই অত্যন্ত সহজে বাস্তবায়ন করা যায়। নিশ্চিতভাবেই, gradient হিসাব করতে chain rule ব্যবহারের ধারণার চেয়ে এটা অনেক বেশি সম্ভাবনাময় দেখায়!

দুর্ভাগ্যবশত, এই পদ্ধতি সম্ভাবনাময় মনে হলেও, code বাস্তবায়ন করলে দেখা যায় এটা অত্যন্ত ধীর। কেন তা বুঝতে কল্পনা করো আমাদের network-এ দশ লক্ষ weight আছে। তখন প্রতিটি আলাদা weight wjw_j-এর জন্য C/wj\partial C / \partial w_j হিসাব করতে আমাদের C(w+ϵej)C(w+\epsilon e_j) হিসাব করতে হবে। অর্থাৎ gradient হিসাব করতে আমাদের cost function দশ লক্ষবার হিসাব করতে হবে, যার জন্য network-এর মধ্য দিয়ে দশ লক্ষ forward pass লাগবে (প্রতিটি training example-এর জন্য)। আমাদের C(w)C(w)-ও হিসাব করতে হবে, তাই সব মিলিয়ে network-এর মধ্য দিয়ে দশ লক্ষ এক বার pass।

Backpropagation-এর চতুরতা হলো এটা আমাদের সব partial derivative C/wj\partial C / \partial w_j একসাথে হিসাব করতে দেয় — কেবল network-এর মধ্য দিয়ে একটিমাত্র forward pass, তারপর একটিমাত্র backward pass দিয়ে। মোটামুটিভাবে, backward pass-এর computational খরচ প্রায় forward pass-এর সমান*। তাই backpropagation-এর মোট খরচ প্রায় network-এর মধ্য দিয়ে কেবল দুটি forward pass করার সমান। এর সাথে তুলনা করো (46)-এর ভিত্তিক পদ্ধতির জন্য আমাদের যে দশ লক্ষ এক বার forward pass লাগত! তাই backpropagation উপর-উপর (46)-এর ভিত্তিক পদ্ধতির চেয়ে বেশি জটিল মনে হলেও, এটা আসলে অনেক, অনেক বেশি দ্রুত।

এই গতিবৃদ্ধি প্রথম পুরোপুরি অনুধাবিত হয় ১৯৮৬ সালে, আর এটা neural network যেসব সমস্যা সমাধান করতে পারত তার পরিধি অনেক বাড়িয়ে দেয়। সেটা, ফলে, neural network ব্যবহার করার লোকের ঢল নামায়। অবশ্যই, backpropagation কোনো সর্বরোগের মহৌষধ নয়। এমনকি ১৯৮০-এর দশকের শেষ দিকেও মানুষ সীমার মুখোমুখি হয়েছিল, বিশেষ করে যখন backpropagation দিয়ে deep neural network — অর্থাৎ অনেক hidden layer সহ network — train করার চেষ্টা করা হয়েছিল। বইয়ের পরে আমরা দেখব কীভাবে আধুনিক computer এবং কিছু চতুর নতুন ধারণা এখন backpropagation দিয়ে এমন deep neural network train করা সম্ভব করেছে।

Backpropagation: বড় ছবি

আমি যেভাবে ব্যাখ্যা করেছি, সেভাবে backpropagation দুটি রহস্য তুলে ধরে। প্রথমত, algorithm-টা আসলে কী করছে? আমরা output থেকে error backpropagate হওয়ার একটা ছবি গড়ে তুলেছি। কিন্তু আমরা কি আরও গভীরে গিয়ে, এই সব matrix ও vector গুণ করার সময় কী ঘটছে সে সম্পর্কে আরও বেশি অন্তর্দৃষ্টি গড়তে পারি? দ্বিতীয় রহস্য হলো — কেউ কীভাবে প্রথমেই backpropagation আবিষ্কার করতে পারল? একটা algorithm-এর ধাপগুলো অনুসরণ করা, এমনকি algorithm-টা কাজ করে তার প্রমাণ অনুসরণ করা — এক কথা। কিন্তু এর মানে এই নয় যে তুমি সমস্যাটা এত ভালোভাবে বুঝেছ যে প্রথমেই algorithm-টা আবিষ্কার করতে পারতে। Backpropagation algorithm আবিষ্কারের দিকে নিয়ে যেতে পারে এমন কোনো যুক্তিযুক্ত চিন্তাধারা কি আছে? এই অংশে আমি এই দুটি রহস্যেরই কথা বলব।

Algorithm-টা কী করছে সে সম্পর্কে আমাদের অন্তর্দৃষ্টি উন্নত করতে, চলো কল্পনা করি আমরা network-এর কোনো একটা weight wjklw^l_{jk}-এ একটা সামান্য পরিবর্তন Δwjkl\Delta w^l_{jk} করলাম:

একটি weight-এ সামান্য পরিবর্তন
Weight wjklw^l_{jk}-এ একটি সামান্য পরিবর্তন Δwjkl\Delta w^l_{jk}

Weight-এ ওই পরিবর্তন সংশ্লিষ্ট neuron-এর output activation-এ একটা পরিবর্তন ঘটাবে:

output activation-এ পরিবর্তন
সংশ্লিষ্ট neuron-এর output activation-এ পরিবর্তন।

সেটা, ফলে, পরের layer-এর সব activation-এ একটা পরিবর্তন ঘটাবে:

পরের layer-এর সব activation-এ পরিবর্তন
পরের layer-এর সব activation-এ পরিবর্তন।

ওই পরিবর্তনগুলো ফলে পরের layer-এ পরিবর্তন ঘটাবে, তারপর তার পরের layer-এ, এভাবে শেষ পর্যন্ত final layer-এ পরিবর্তন ঘটাবে, আর তারপর cost function-এ:

final layer ও cost function পর্যন্ত পরিবর্তন ছড়িয়ে পড়া
পরিবর্তনগুলো final layer ও cost function পর্যন্ত ছড়িয়ে পড়ে।

cost-এ পরিবর্তন ΔC\Delta C এবং weight-এ পরিবর্তন Δwjkl\Delta w^l_{jk}-এর সম্পর্ক নিচের equation দিয়ে দেওয়া হয়

ΔCCwjklΔwjkl.(47)\Delta C \approx \frac{\partial C}{\partial w^l_{jk}} \Delta w^l_{jk}.\tag{47}

এটা ইঙ্গিত করে যে Cwjkl\frac{\partial C}{\partial w^l_{jk}} হিসাব করার একটা সম্ভাব্য পদ্ধতি হলো — wjklw^l_{jk}-এ একটা সামান্য পরিবর্তন কীভাবে ছড়িয়ে পড়ে CC-এ একটা সামান্য পরিবর্তন ঘটায় তা যত্ন করে অনুসরণ করা। যদি আমরা তা করতে পারি, পথের সবকিছু সহজে-হিসাব-করা-যায় এমন রাশিতে প্রকাশ করতে সতর্ক থাকি, তবে আমরা C/wjkl\partial C / \partial w^l_{jk} হিসাব করতে পারব।

চলো এটা করার চেষ্টা করি। পরিবর্তন Δwjkl\Delta w^l_{jk} lthl^{\rm th} layer-এর jthj^{\rm th} neuron-এর activation-এ একটা সামান্য পরিবর্তন Δajl\Delta a^{l}_j ঘটায়। এই পরিবর্তন দেওয়া হয়

ΔajlajlwjklΔwjkl.(48)\Delta a^l_j \approx \frac{\partial a^l_j}{\partial w^l_{jk}} \Delta w^l_{jk}.\tag{48}

activation-এ পরিবর্তন Δajl\Delta a^l_{j} পরের layer, অর্থাৎ (l+1)th(l+1)^{\rm th} layer-এর সব activation-এ পরিবর্তন ঘটাবে। আমরা সেগুলোর কেবল একটা activation, ধরো aql+1a^{l+1}_q, কীভাবে প্রভাবিত হয় তার উপর মনোযোগ দেব,

একটি activation a^{l+1}_q-এ পরিবর্তন
পরের layer-এর একটি activation aql+1a^{l+1}_q-এ পরিবর্তন।

আসলে, এটা নিচের পরিবর্তন ঘটাবে:

Δaql+1aql+1ajlΔajl.(49)\Delta a^{l+1}_q \approx \frac{\partial a^{l+1}_q}{\partial a^l_j} \Delta a^l_j.\tag{49}

Equation (48) থেকে রাশি বসিয়ে আমরা পাই:

Δaql+1aql+1ajlajlwjklΔwjkl.(50)\Delta a^{l+1}_q \approx \frac{\partial a^{l+1}_q}{\partial a^l_j} \frac{\partial a^l_j}{\partial w^l_{jk}} \Delta w^l_{jk}.\tag{50}

অবশ্যই, পরিবর্তন Δaql+1\Delta a^{l+1}_q, ফলে, পরের layer-এর activation-গুলোতে পরিবর্তন ঘটাবে। আসলে আমরা wjklw^l_{jk} থেকে CC পর্যন্ত network জুড়ে একটা পথ কল্পনা করতে পারি, যেখানে activation-এর প্রতিটি পরিবর্তন পরের activation-এ পরিবর্তন ঘটায়, আর সবশেষে output-এ cost-এ পরিবর্তন ঘটায়। যদি পথটি activation ajl,aql+1,,anL1,amLa^l_j, a^{l+1}_q, \ldots, a^{L-1}_n, a^L_m-এর মধ্য দিয়ে যায় তবে ফলাফল-রাশি হলো

ΔCCamLamLanL1anL1apL2aql+1ajlajlwjklΔwjkl,(51)\Delta C \approx \frac{\partial C}{\partial a^L_m} \frac{\partial a^L_m}{\partial a^{L-1}_n} \frac{\partial a^{L-1}_n}{\partial a^{L-2}_p} \ldots \frac{\partial a^{l+1}_q}{\partial a^l_j} \frac{\partial a^l_j}{\partial w^l_{jk}} \Delta w^l_{jk},\tag{51}

অর্থাৎ আমরা যত অতিরিক্ত neuron-এর মধ্য দিয়ে গিয়েছি প্রতিটির জন্য একটা a/a\partial a / \partial a ধরনের পদ পেয়েছি, সেইসাথে শেষে C/amL\partial C/\partial a^L_m পদ। এটা network-এর এই নির্দিষ্ট পথ ধরে activation-এর পরিবর্তনের কারণে CC-এর পরিবর্তন প্রকাশ করে। অবশ্যই, wjklw^l_{jk}-এ একটা পরিবর্তন ছড়িয়ে পড়ে cost-কে প্রভাবিত করতে পারে এমন অনেক পথ আছে, আর আমরা কেবল একটা পথ বিবেচনা করছিলাম। CC-এর মোট পরিবর্তন হিসাব করতে এটা যুক্তিযুক্ত যে weight ও final cost-এর মধ্যকার সম্ভাব্য সব পথের উপর আমাদের যোগফল নেওয়া উচিত, অর্থাৎ,

ΔCmnpqCamLamLanL1anL1apL2aql+1ajlajlwjklΔwjkl,(52)\Delta C \approx \sum_{mnp\ldots q} \frac{\partial C}{\partial a^L_m} \frac{\partial a^L_m}{\partial a^{L-1}_n} \frac{\partial a^{L-1}_n}{\partial a^{L-2}_p} \ldots \frac{\partial a^{l+1}_q}{\partial a^l_j} \frac{\partial a^l_j}{\partial w^l_{jk}} \Delta w^l_{jk},\tag{52}

যেখানে আমরা পথ ধরে মধ্যবর্তী neuron-গুলোর সব সম্ভাব্য পছন্দের উপর যোগফল নিয়েছি। (47)-এর সাথে তুলনা করে আমরা দেখি যে

Cwjkl=mnpqCamLamLanL1anL1apL2aql+1ajlajlwjkl.(53)\frac{\partial C}{\partial w^l_{jk}} = \sum_{mnp\ldots q} \frac{\partial C}{\partial a^L_m} \frac{\partial a^L_m}{\partial a^{L-1}_n} \frac{\partial a^{L-1}_n}{\partial a^{L-2}_p} \ldots \frac{\partial a^{l+1}_q}{\partial a^l_j} \frac{\partial a^l_j}{\partial w^l_{jk}}.\tag{53}

এখন, Equation (53) জটিল দেখায়। তবে এর একটা সুন্দর স্বজ্ঞাত ব্যাখ্যা আছে। আমরা network-এর একটা weight সাপেক্ষে CC-এর পরিবর্তনের হার হিসাব করছি। equation আমাদের বলে — network-এর দুটি neuron-এর মধ্যকার প্রতিটি edge-এর সাথে একটা rate factor যুক্ত, যা কেবল একটা neuron-এর activation-এর সাপেক্ষে আরেকটা neuron-এর activation-এর partial derivative। প্রথম weight থেকে প্রথম neuron পর্যন্ত edge-এর rate factor হলো ajl/wjkl\partial a^{l}_j / \partial w^l_{jk}। একটা পথের rate factor হলো সেই পথ ধরে rate factor-গুলোর গুণফল। আর মোট পরিবর্তনের হার C/wjkl\partial C / \partial w^l_{jk} হলো প্রাথমিক weight থেকে চূড়ান্ত cost পর্যন্ত সব পথের rate factor-গুলোর যোগফল। এই প্রক্রিয়াটি এখানে একটা একক পথের জন্য দেখানো হলো:

একটি পথের rate factor
একটি একক পথের rate factor — edge-গুলোর partial derivative-এর গুণফল।

এতক্ষণ আমি যা দিয়েছি তা হলো একটা heuristic যুক্তি, network-এর একটা weight বিচলিত করলে কী ঘটছে তা ভাবার একটা উপায়। চলো এই যুক্তি আরও বিকশিত করতে একটা চিন্তাধারার রূপরেখা দিই। প্রথমত, তুমি Equation (53)-এর সব পৃথক partial derivative-এর জন্য সুস্পষ্ট রাশি বের করতে পারতে। একটু calculus দিয়ে সেটা সহজেই করা যায়। তা করার পর, তুমি index-এর উপর সব যোগফলকে কীভাবে matrix গুণ হিসেবে লেখা যায় তা বের করার চেষ্টা করতে পারতে। এটা ক্লান্তিকর, আর কিছুটা অধ্যবসায় দরকার, কিন্তু কোনো অসাধারণ অন্তর্দৃষ্টি লাগে না। এসব করার পর, এবং যতটা সম্ভব সরল করার পর, তুমি যা আবিষ্কার করবে তা হলো — তুমি ঠিক backpropagation algorithm-এই পৌঁছে গেছ! তাই backpropagation algorithm-কে তুমি এই সব পথের জন্য rate factor-এর যোগফল হিসাব করার একটা উপায় হিসেবে ভাবতে পারো। কিংবা, একটু অন্যভাবে বললে, backpropagation algorithm হলো weight (ও bias)-এ ছোট বিচলন network-এর মধ্য দিয়ে ছড়িয়ে পড়ে, output-এ পৌঁছে, তারপর cost-কে প্রভাবিত করার সময় সেগুলোর হিসাব রাখার একটা চতুর উপায়।

এখন, আমি এখানে এই পুরোটা বিস্তারিত করব না। এটা এলোমেলো, আর সব খুঁটিনাটি সম্পন্ন করতে যথেষ্ট যত্ন দরকার। তুমি যদি একটা চ্যালেঞ্জের জন্য তৈরি থাকো, তবে এটা চেষ্টা করতে হয়তো উপভোগ করবে। আর তা না করলেও, আশা করি এই চিন্তাধারা তোমাকে backpropagation কী সাধন করছে সে সম্পর্কে কিছু অন্তর্দৃষ্টি দেবে।

আর সেই অন্য রহস্যটা — backpropagation প্রথমে কীভাবে আবিষ্কার হতে পারত? আসলে তুমি যদি আমি যে পদ্ধতির রূপরেখা দিলাম তা অনুসরণ করো, তবে তুমি backpropagation-এর একটা প্রমাণ আবিষ্কার করবে। দুর্ভাগ্যবশত, সেই প্রমাণ এই অধ্যায়ে আগে বর্ণিত প্রমাণের চেয়ে বেশ খানিকটা দীর্ঘ ও জটিল। তাহলে ওই সংক্ষিপ্ত (কিন্তু বেশি রহস্যময়) প্রমাণটা কীভাবে আবিষ্কার হলো? দীর্ঘ প্রমাণের সব খুঁটিনাটি লিখে ফেললে তুমি যা পাবে তা হলো — পরে দেখা যায় কয়েকটা সুস্পষ্ট সরলীকরণ তোমার চোখের সামনেই দাঁড়িয়ে আছে। তুমি সেই সরলীকরণগুলো করো, একটা ছোট প্রমাণ পাও, আর সেটা লিখে ফেলো। তারপর আরও কয়েকটা সুস্পষ্ট সরলীকরণ তোমার চোখে পড়ে। তাই তুমি আবার পুনরাবৃত্তি করো। কয়েকটা পুনরাবৃত্তির পর ফল হলো সেই প্রমাণ যা আমরা আগে দেখেছি* — সংক্ষিপ্ত, কিন্তু কিছুটা দুর্বোধ্য, কারণ এর নির্মাণের সব দিকনির্দেশনা মুছে ফেলা হয়েছে! অবশ্যই আমি তোমাকে এ ব্যাপারে আমার উপর ভরসা করতে বলছি, কিন্তু আগের প্রমাণের উৎপত্তিতে সত্যিই কোনো বড় রহস্য নেই। এটা কেবল এই অংশে আমার রূপরেখা-দেওয়া প্রমাণটাকে সরল করার প্রচুর পরিশ্রম মাত্র।