Chapter 12 of 18
How error flows backward to teach every weight in the network
এই কোর্সের আগের প্রতিটা চ্যাপ্টারই এই মুহূর্তের দিকে তৈরি হচ্ছিল। Forward propagation একটা ইনপুটকে একটা প্রেডিকশনে পরিণত করে। Loss function সেই প্রেডিকশনকে একটা মাত্র সংখ্যায় পরিণত করে যা এটা কতটা ভুল ছিল তা মাপে। Gradient descent বলে: সেই সংখ্যাটা নিন, প্রতিটা weight-এর জন্য একটা gradient হিসাব করুন, আর প্রতিটা weight-কে loss কমাতে এর gradient-এর বিপরীতে ঠেলে দিন। কিন্তু একটা ফাঁক আছে যা এই চ্যাপ্টারগুলোর কোনোটাই আসলে বন্ধ করেনি: নেটওয়ার্কের ভেতরে তিন লেয়ার গভীরে পোঁতা একটা weight-এর সাপেক্ষে loss-এর gradient কীভাবে হিসাব করবেন?
একদম শেষ লেয়ারের জন্য, এটা খুব কঠিন না — loss সরাসরি সেই লেয়ারের আউটপুট থেকে হিসাব করা, তাই loss আর সেই লেয়ারের weight-এর মধ্যে সম্পর্কটা মোটামুটি সরাসরি। কিন্তু পাঁচ-লেয়ারের একটা নেটওয়ার্কের প্রথম হিডেন লেয়ারের একটা weight বিবেচনা করুন। সেই weight প্রথম লেয়ারের আউটপুটকে প্রভাবিত করে, যা দ্বিতীয় লেয়ারের আউটপুটকে প্রভাবিত করে, যা তৃতীয়টাকে প্রভাবিত করে, এভাবে চলতে থাকে, একদম চূড়ান্ত প্রেডিকশন পর্যন্ত আর তারপরই loss পর্যন্ত। সেই একটা প্রাথমিক weight-এর চূড়ান্ত loss-এর উপর প্রভাব চারটা লেয়ারের ইন্টারমিডিয়েট রূপান্তরের নিচে পোঁতা।
Backpropagation হলো সেই অ্যালগরিদম যা কার্যকরভাবে নেটওয়ার্কের প্রতিটা weight আর bias-এর সাপেক্ষে loss-এর gradient হিসাব করে, নেটওয়ার্ক যতই গভীর হোক না কেন, আউটপুট লেয়ার থেকে ইনপুট লেয়ার পর্যন্ত পেছনের দিকে কাজ করে।
Backpropagation ছাড়া, একটা deep নেটওয়ার্কের জন্য gradient হিসাব করা কম্পিউটেশনালি বিপর্যয়কর হতো। একটা সরল পদ্ধতি — একটা weight সামান্য ঠেলে, পুরো forward pass আবার চালিয়ে, loss কতটা বদলেছে সেটা দেখে — নেটওয়ার্কের প্রতিটা weight-এর জন্য আলাদাভাবে পুনরাবৃত্তি করতে হতো, প্রতি weight-এ একটা পুরো forward pass। দশ লক্ষ weight-এর একটা নেটওয়ার্কের শুধু একটা gradient ধাপ হিসাব করতেই দশ লক্ষ অতিরিক্ত forward pass দরকার হতো। Backpropagation ঠিক একই gradient হিসাব করে নেটওয়ার্কের মধ্য দিয়ে শুধু একটা backward pass ব্যবহার করে, যতই weight থাকুক না কেন।
Forward propagation যদি একগুচ্ছ পাইপের মধ্য দিয়ে নিচের দিকে প্রবাহিত পানি হয় — উপরে ইনপুট, নিচে প্রেডিকশন — backpropagation হলো সেই একই পাইপগুলোর মধ্য দিয়ে উপরের দিকে ফিরে যাওয়া তথ্য, প্রতিটা জংশন রিপোর্ট করছে ঠিক কতটা এটা নিচের চূড়ান্ত ফলাফলে অবদান রেখেছে।
Backpropagation-এর কার্যকারিতা আসে ক্যালকুলাসের একটা মাত্র অংশ থেকে: chain rule। Chain rule বর্ণনা করে কীভাবে এমন একটা ফাংশনের ডেরিভেটিভ হিসাব করতে হয় যা বেশ কয়েকটা ছোট ফাংশনকে একসাথে কম্পোজ করে বানানো — ঠিক সেই পরিস্থিতি যা একটা deep নেটওয়ার্ক তৈরি করে, কারণ প্রতিটা লেয়ারের আউটপুট সরাসরি পরের লেয়ারের হিসাবে খাওয়ায়।
একটা সহজ চেইন কল্পনা করুন: -কে প্রভাবিত করে, আর -কে প্রভাবিত করে। যদি জানতে চান -এ একটা ছোট পরিবর্তন শেষ পর্যন্ত -কে কতটা প্রভাবিত করে, chain rule বলে দুইটা স্থানীয় প্রভাবকে একসাথে গুণ করুন:
মাপে -এ প্রতি ইউনিট পরিবর্তনে কতটা বদলায় — একটা নিখুঁত স্থানীয় সম্পর্ক, চেইনের একটা ধাপ। মাপে -এ প্রতি ইউনিট পরিবর্তনে কতটা বদলায় — আরেকটা নিখুঁত স্থানীয় সম্পর্ক, পরের ধাপ। এদের একসাথে গুণ করলে থেকে পর্যন্ত পুরো সম্মিলিত প্রভাব পাওয়া যায়, যদিও কখনো সরাসরি -কে স্পর্শ করে না।
ধরুন আর । তাহলে আর । Chain rule অনুযায়ী, — আর সত্যিই, যেহেতু , একটা সরাসরি হিসাব নিশ্চিত করে। Chain rule-এর -এর সাপেক্ষে -এর সরাসরি সূত্র আদৌ দরকার হয়নি; শুধু প্রতিটা ধাপের স্থানীয় ডেরিভেটিভই দরকার হয়েছে।
একটা নিউরাল নেটওয়ার্ক ঠিক এই ধরনের একটা চেইন, শুধু অনেক বেশি লিংকসহ: loss নির্ভর করে আউটপুট লেয়ারের অ্যাক্টিভেশনের উপর, যা নির্ভর করে আউটপুট লেয়ারের ওয়েটেড সামের উপর, যা নির্ভর করে আগের লেয়ারের অ্যাক্টিভেশনের উপর, যা নির্ভর করে সেই লেয়ারের ওয়েটেড সামের উপর, এভাবে চলতে থাকে, একদম ইনপুট পর্যন্ত। Backpropagation এই পুরো চেইন জুড়ে, লেয়ারে লেয়ারে, chain rule বারবার প্রয়োগ করে বের করে কীভাবে যেকোনো weight-এর একটা পরিবর্তন — নেটওয়ার্কের যেকোনো জায়গায় — শেষ পর্যন্ত একদম শেষে loss-কে প্রভাবিত করে।
একটা deep নেটওয়ার্কের চেইনে অনেক লিংক থাকে, কিন্তু প্রতিটা ইন্ডিভিজুয়াল লিংক সহজ: একটা ওয়েটেড সাম তারপর একটা অ্যাক্টিভেশন ফাংশন, ঠিক যেমনটা Forward Propagation চ্যাপ্টারে দেখা হয়েছে। Chain rule backpropagation-কে যতই লম্বা চেইন সামলাতে দেয়, শুধু এই সহজ, স্থানীয় ডেরিভেটিভগুলো হিসাব করে, একবারে একটা লিংক, আর পেছনের দিকে যেতে যেতে এদের একসাথে গুণ করে।
Backpropagation আউটপুট লেয়ার থেকে শুরু করে gradient হিসাব করে আর ইনপুট লেয়ারের দিকে পেছনের দিকে কাজ করে — forward propagation-এর বিপরীত দিক, যেখান থেকে ঠিক নামটা এসেছে।
প্রক্রিয়াটা শুরু হয় আউটপুট লেয়ারের অ্যাক্টিভেশনের সাপেক্ষে loss কীভাবে বদলায় সেটা হিসাব করে — এটা সাধারণত সরল, কারণ loss function (Loss Functions চ্যাপ্টার থেকে) সরাসরি সেই অ্যাক্টিভেশন আর সত্যিকারের লেবেলের ভিত্তিতে সংজ্ঞায়িত। এই রাশিটাকে আউটপুট লেয়ারের "error signal" বলা যাক।
সেখান থেকে, backpropagation একবারে একটা লেয়ার পেছনের দিকে যায়। প্রতিটা লেয়ারে, এটা chain rule ব্যবহার করে দুইটা জিনিস হিসাব করে: এই লেয়ারের প্রতিটা weight loss-এ কতটা অবদান রেখেছে (সেই weight আপডেট করতে ব্যবহৃত), আর প্রতিটা নিউরনের ইনপুট loss-এ কতটা অবদান রেখেছে (আরও পেছনে পাঠানোর জন্য একটা error signal, এর আগের লেয়ারে)।
এই সূত্রটা ঘন দেখাতে পারে, কিন্তু এর ভেতরের আইডিয়াটা সহজ: হলো পরের লেয়ারে (আউটপুটের কাছাকাছি) ইতিমধ্যে হিসাব করা error signal, হলো লেয়ার -কে লেয়ার -এর সাথে যুক্ত করা weight ম্যাট্রিক্স, আর হলো লেয়ার -এ অ্যাক্টিভেশন ফাংশনের ডেরিভেটিভ। এদের একসাথে গুণ করলে "পরের লেয়ার কতটা ভুল ছিল" "এই লেয়ার কতটা ভুল ছিল"-তে রূপান্তরিত হয় — ঠিক একটা chain-rule ধাপ, একবারে একটা সংখ্যার বদলে একসাথে একটা পুরো লেয়ারের নিউরনে প্রয়োগ করা।
Gradient flow হলো একটা error signal পেছনের দিকে পাঠানোর প্রক্রিয়া, লেয়ারে লেয়ারে, আউটপুটের error-কে chain rule ব্যবহার করে প্রতিটা হিডেন লেয়ারের error-এ রূপান্তরিত করে।
এই backward pass প্রতিটা লেয়ারকে ঠিক একবার স্পর্শ করে, উল্টো ক্রমে — আউটপুট লেয়ারের error হিসাব হয়, শেষের-আগের লেয়ারের error হিসাব করতে ব্যবহৃত হয়, যা তৃতীয়-শেষ লেয়ারের error হিসাব করতে ব্যবহৃত হয়, এভাবে চলতে থাকে, যতক্ষণ না একদম প্রথম হিডেন লেয়ারের নিজস্ব error signal থাকে। প্রতিটা লেয়ারের শুধু তার ঠিক পরের লেয়ার থেকে হস্তান্তরিত error signal, প্লাস নিজস্ব স্থানীয় weight আর অ্যাক্টিভেশন ডেরিভেটিভ দরকার — কোনো লেয়ারের তার নিকটতম প্রতিবেশীর চেয়ে বেশি দূরে পেছনে বা সামনে পৌঁছানোর দরকার নেই।
যেহেতু সরাসরি backward pass-এ দেখা যায়, অ্যাক্টিভেশন ফাংশনের বাছাই সরাসরি নির্ধারণ করে gradient একটা deep নেটওয়ার্কের মধ্য দিয়ে পেছনে কতটা ভালোভাবে প্রবাহিত হয়। এমন অ্যাক্টিভেশন ফাংশন যা বড় ইনপুটের জন্য স্যাচুরেট (সমতল হয়ে যায়), যেমন sigmoid বা tanh, সেই সমতল অঞ্চলে ডেরিভেটিভ প্রায় শূন্যের কাছাকাছি — অনেক লেয়ার জুড়ে অনেক ছোট সংখ্যা একসাথে গুণ করলে gradient শূন্যের দিকে সংকুচিত হয়ে যেতে পারে যখন এটা প্রাথমিক লেয়ারে পৌঁছায়, একটা সমস্যা যাকে বলা হয় vanishing gradient problem, এটা একটা কারণ কেন deep হিডেন লেয়ারে ReLU বেশি জনপ্রিয়।
একবার একটা নির্দিষ্ট লেয়ারে error signal হিসাব হয়ে গেলে, সেই লেয়ারের weight-এর জন্য আসল gradient পাওয়া একটা সংক্ষিপ্ত শেষ ধাপ: error signal-কে সেই লেয়ারে খাওয়ানো অ্যাক্টিভেশন দিয়ে গুণ করুন।
এখানে হলো আগের লেয়ার থেকে আসা অ্যাক্টিভেশন — সেই একই মান যা forward pass-এর সময় এই লেয়ারের ওয়েটেড সাম হিসাব করতে ব্যবহৃত হয়েছিল। স্বজ্ঞাতভাবে, এটা বোঝা যায়: loss-এ একটা weight-এর প্রভাব নির্ভর করে দুইটা জিনিসের উপর, এই লেয়ারের আউটপুট কতটা ভুল ছিল () আর সেই ইনপুট আসলে কতটা জোরালোভাবে উপস্থিত ছিল প্রথম জায়গায় () — শূন্যের কাছাকাছি একটা ইনপুটের সাথে যুক্ত একটা weight নিজের মান যাই হোক না কেন error-এ বেশি অবদান রাখতে পারত না।
একটা একই ধরনের, সহজ হিসাব শুধু থেকে সরাসরি প্রতিটা লেয়ারের bias-এর জন্য gradient দেয়, কারণ একটা bias কোনো ইনকামিং অ্যাক্টিভেশনকে গুণ করে না।
এখন পুরো নেটওয়ার্ক জুড়ে প্রতিটা weight আর bias-এর gradient হিসাব হয়ে গেলে, Gradient & Gradient Descent চ্যাপ্টারের আপডেট ধাপ সরাসরি প্রয়োগ হয়, লেয়ারে লেয়ারে:
Weight updates প্রতিটা লেয়ারের পেছনে-প্রবাহিত error signal-কে সেই লেয়ারে প্রবেশ করা অ্যাক্টিভেশনের সাথে মিলিয়ে একটা gradient তৈরি করে, যা gradient descent তারপর প্রতিটা weight আর bias-কে কম loss-এর দিকে ঠেলতে ব্যবহার করে।
একসাথে মিলিয়ে, একটা পুরো ট্রেনিং ধাপ দেখতে এরকম: forward propagation একটা প্রেডিকশন হিসাব করে, loss function এটা কতটা ভুল ছিল তা মাপে, backpropagation সেই error-কে প্রতিটা লেয়ারের মধ্য দিয়ে পেছনে প্রবাহিত করে পথে gradient হিসাব করে, আর gradient descent সেই gradient ব্যবহার করে একসাথে প্রতিটা weight আর bias আপডেট করে। এই চক্রটা বারবার পুনরাবৃত্তি করুন, অনেক ট্রেনিং উদাহরণ জুড়ে, আর নেটওয়ার্কের weight-গুলো ধীরে ধীরে এমন মানের দিকে সরে যায় যা সঠিক প্রেডিকশন তৈরি করে।
এখানে সীমানাটা নিয়ে সঠিক হওয়া গুরুত্বপূর্ণ: backpropagation-এর পুরো কাজ কার্যকরভাবে gradient হিসাব করা। এটা ঠিক করে না কতটা বড় পদক্ষেপ নিতে হবে (এটা learning rate-এর কাজ) আর এটা ঠিক করে না কোন উদাহরণ ব্যবহার করতে হবে (এটা Gradient & Gradient Descent চ্যাপ্টার থেকে batch স্ট্র্যাটেজির কাজ)। Backprop শুধু উত্তর দেয় "প্রতিটা প্যারামিটারের জন্য loss কোন দিকে, আর কতটা খাড়াভাবে বদলায়" — গাণিতিকভাবে যতটা সম্ভব কার্যকরভাবে।
একটা ছোট কনক্রিট নেটওয়ার্কের মধ্য দিয়ে backpropagation ট্রেস করলে বিমূর্ত প্রবাহটা কনক্রিট হয়ে ওঠে। একটা নেটওয়ার্ক নিন একটা ইনপুট সহ, weight আর অ্যাক্টিভেশন -সহ একটা হিডেন নিউরন, আর weight -সহ একটা আউটপুট নিউরন যা প্রেডিকশন তৈরি করে, squared error loss ব্যবহার করে সত্যিকারের মান -এর বিরুদ্ধে ট্রেন করা।
Forward pass ক্রমানুসারে হিসাব করে: , তারপর , তারপর , তারপর ।
Backpropagation ঠিক এই একই চেইন উল্টো ক্রমে হাঁটে:
প্যাটার্নটা খেয়াল করুন: প্রতিটা ধাপের শুধু আগের ধাপে হিসাব করা error signal দরকার, প্লাস একটা নিখুঁত স্থানীয় তথ্য (একটা অ্যাক্টিভেশন, একটা weight, বা একটা অ্যাক্টিভেশন ডেরিভেটিভ) যা চেইনের সেই বিন্দুতে ঠিক পাওয়া যায়। ধাপ ৪-এর কোনোভাবে সরাসরি loss পর্যন্ত "পুরো পথ পেছনে পৌঁছানোর" দরকার হয়নি — chain rule সেই তথ্যটা পেছনে বহন করেছে, একবারে একটা স্থানীয় গুণ, ঠিক যেমনটা Chain Rule-এর ইনটুইশন সেকশনে বর্ণনা করা হয়েছে।
একটা আসল নেটওয়ার্ক প্রতিটা নিউরনে, প্রতিটা লেয়ারে, ঠিক এই একই চার-ধাপের প্যাটার্ন একসাথে পুনরাবৃত্তি করে — লক্ষ লক্ষ ছোট স্থানীয় গুণ, একসাথে চেইন করা loss থেকে একদম প্রথম weight পর্যন্ত পেছনে। গণিতটা এই worked example-এর চেয়ে কনসেপচুয়ালি কখনো কঠিন হয় না; এটা শুধু স্কেলে বড় হয়।