পাঠ ২৬ · ৫৭-এর মধ্যে · মডিউল ৬
Home / Courses / Digital Signal Processing / কেন FFT

কেন FFT — DFT-এর কম্পিউটেশনাল কমপ্লেক্সিটি

Why FFT — computational complexity of the DFT
৯ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • ডিরেক্ট DFT-এর সূত্র থেকে কীভাবে O(N²) অপারেশন কাউন্ট বের হয়, ধাপে ধাপে
  • একটি সত্যিকারের, চলমান Python ডেমো যা গুণন ও যোগ অপারেশন গুনে দেখায়
  • N বাড়ার সাথে সাথে খরচ কীভাবে বাড়ে — একটি বাস্তব কম্পিউটেড টেবিল থেকে প্যাটার্ন বের করা
  • কেন বড় সিগন্যালের জন্য (যেমন অডিও, ইমেজ প্রসেসিং) O(N²) ব্যবহারিকভাবে অসম্ভব হয়ে ওঠে

১ · ডিরেক্ট DFT-এর খরচ কোথা থেকে আসে

M5-এ (L22) আমরা DFT-এর সংজ্ঞা শিখেছি:

$$X[k] = \sum_{n=0}^{N-1} x[n] \, e^{-j 2\pi kn/N}, \qquad k = 0, 1, \dots, N-1$$

লক্ষ্য করুন — একটি নির্দিষ্ট k-এর জন্য X[k] বের করতে Nটি পদ যোগ করতে হয়, আর প্রতিটি পদের জন্য একটি জটিল গুণন (x[n] আর exp(...)-এর গুণফল) লাগে। অর্থাৎ একটি k-এর জন্য Nটি গুণন এবং N-1টি যোগ। এখন এটি সবগুলো k = 0 থেকে N-1 পর্যন্ত করতে হবে — তাই মোট গুণন সংখ্যা N × N = N², আর মোট যোগ সংখ্যা N × (N-1)। দুটো মিলিয়ে মোট অপারেশন 2N² - N — যা বড় N-এর জন্য মূলত N²-এর সমানুপাতিক। এটাই DFT-কে O(N²) অ্যালগরিদম বলার কারণ।

প্রতি বিনে N গুণন
X[k] বের করতে x[0], x[1], ..., x[N-1] প্রতিটিকে সংশ্লিষ্ট exp(-j2πkn/N) দিয়ে গুণ করতে হয় — মোট N গুণন।
N বিন × N গুণন = N²
প্রতিটি k-এর জন্য আলাদাভাবে N গুণন লাগে, আর k-এর মান N রকম — তাই মোট গুণন N².
N দ্বিগুণ = খরচ চারগুণ
যেহেতু খরচ N-এর বর্গের সমানুপাতিক, N দ্বিগুণ করলে খরচ (2N)² = 4N² — অর্থাৎ চারগুণ, দ্বিগুণ নয়।

২ · সত্যিকারের গণনা — কোডে গুণন ও যোগ গোনা

অনুমান না করে, চলুন সরাসরি কোডের ভেতরে প্রতিটি গুণন ও যোগ অপারেশন গুনে দেখি। নিচের counted_dft ফাংশনটি M5/L22-এর ডিরেক্ট DFT সূত্রই প্রয়োগ করে, কিন্তু প্রতিটি গুণন ও যোগের সাথে সাথে একটি কাউন্টার বাড়িয়ে দেয়:

Python
import cmath, math

def counted_dft(x):
    N = len(x)
    X = [0j] * N
    mults = 0
    adds = 0
    for k in range(N):
        total = 0j
        for n in range(N):
            term = x[n] * cmath.exp(-2j * math.pi * k * n / N)
            mults += 1
            total += term
            if n > 0:
                adds += 1
        X[k] = total
    return X, mults, adds

print(f"{'N':>5} | {'জটিল গুণন':>10} | {'জটিল যোগ':>10} | {'মোট অপারেশন':>13} | {'N^2':>6}")
prev_total = None
for N in [8, 16, 32, 64, 128]:
    x = [math.sin(2 * math.pi * 3 * n / N) for n in range(N)]
    X, mults, adds = counted_dft(x)
    total = mults + adds
    ratio = f"{total/prev_total:.2f}x" if prev_total else "—"
    print(f"{N:>5} | {mults:>10} | {adds:>10} | {total:>13} | {N*N:>6}   (আগের তুলনায় {ratio})")
    prev_total = total

    
কোডটি রান করলে দেখা যায়: N=8-এ ৬৪টি গুণন + ৫৬টি যোগ = ১২০টি মোট অপারেশন; N=16-এ ২৫৬ + ২৪০ = ৪৯৬; N=32-এ ১০২৪ + ৯৯২ = ২০১৬; N=64-এ ৪০৯৬ + ৪০৩২ = ৮১২৮; আর N=128-এ ১৬৩৮৪ + ১৬২৫৬ = ৩২৬৪০টি মোট অপারেশন। প্রতিটি ধাপে N দ্বিগুণ হওয়ার সাথে সাথে মোট অপারেশন প্রায় ৪.১৩x, ৪.০৬x, ৪.০৩x, ৪.০২x — অর্থাৎ N যত বড় হয়, অনুপাত তত নিখুঁতভাবে ৪-এর কাছাকাছি যায়, যা 2N² - N সূত্রে বড় N-এর জন্য N² পদের প্রাধান্য থেকেই আসে (গুণন সংখ্যা ঠিক N², যোগ সংখ্যা N(N-1) — দুটোই কোডের গোনা মান হুবহু মিলে যায়)।

৩ · N বড় হলে বাস্তবে কী দাঁড়ায়

উপরের কোডে গোনা মোট অপারেশন সংখ্যা ঠিক 2N² - N সূত্র মেনে চলে (যাচাই: N=128-এ 2×128² - 128 = 32768 - 128 = 32640 — কোডের আউটপুটের সাথে হুবহু মিলে যায়)। এই যাচাই করা সূত্র ব্যবহার করে বড় N-এর জন্য কী হবে তা হিসেব করা যায় (Pyodide sandbox দ্রুত রাখতে কোডে আমরা N=128 পর্যন্তই সীমাবদ্ধ রাখছি, কিন্তু সূত্রটি বৈধ থাকে):

  • N = 512 → 2×512² - 512 = ৫,২৩,৭৭৬টি অপারেশন
  • N = 1024 (একটি সাধারণ অডিও FFT সাইজ) → 2×1024² - 1024 = ২১,০৯,১২৮টি অপারেশন
  • N = 4096 → 2×4096² - 4096 = ৩,৩৫,৪৪,৩২০টি অপারেশন

একটি বাস্তব অডিও স্ট্রিমে প্রতি সেকেন্ডে হাজার হাজার বার এই ধরনের রূপান্তর করতে হতে পারে (যেমন M10-এ শেখা STFT/স্পেকট্রোগ্রাম)। N=4096-এ প্রতিটি DFT কল করতে প্রায় ৩ কোটি ৩৫ লক্ষ অপারেশন লাগলে, এটি বাস্তব-সময়ে (real-time) চালানো ব্যবহারিকভাবে অসম্ভব হয়ে দাঁড়ায়।

মূল কথা · Key takeaway

ডিরেক্ট DFT সূত্র নিজেই সম্পূর্ণ সঠিক — সমস্যা নির্ভুলতায় নয়, খরচে। O(N²) বৃদ্ধির হার মানে N বড় হলে খরচ দ্রুত অসহনীয় হয়ে যায়। পরবর্তী পাঠে (L27) আমরা দেখব কীভাবে DFT সূত্রের ভেতরের একটি স্মার্ট প্রতিসাম্য কাজে লাগিয়ে ঠিক একই ফলাফল মাত্র O(N log N) অপারেশনে বের করা যায় — এই অ্যালগরিদমটির নাম Fast Fourier Transform (FFT)।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ কোডের আউটপুটে N=8 থেকে N=128 পর্যন্ত অনুপাত ৪.১৩x থেকে কমে ৪.০২x-এ নেমে এসেছে — একদম ঠিক ৪x নয় কেন?

কারণ মোট অপারেশন সূত্র 2N² - N-এ একটি -N পদও আছে, যা N²-এর তুলনায় ছোট কিন্তু ছোট N-এ তুলনামূলক বেশি প্রভাব ফেলে। N যত বড় হয়, -N পদটি 2N²-এর তুলনায় তত নগণ্য হয়ে যায়, তাই অনুপাত ঠিক ৪-এর দিকে ধাবিত হয় (এটাই "big-O" বিশ্লেষণে নিম্ন-ক্রমের পদ উপেক্ষা করার কারণ — শুধু আধিপত্যকারী পদ, এখানে N², দীর্ঘমেয়াদী আচরণ নির্ধারণ করে)।

প্র ০২ যদি একটি অ্যালগরিদম O(N log N) হয়, তাহলে N দ্বিগুণ করলে খরচ ঠিক কতগুণ বাড়বে — ৪ গুণ, নাকি তার কম?

তার অনেক কম। N log₂N-এ N দ্বিগুণ করলে নতুন খরচ 2N × log₂(2N) = 2N × (log₂N + 1) — এটি পুরনো খরচ N log₂N-এর তুলনায় সামান্য বেশি ২ গুণ (ঠিক ২ গুণ নয়, তার চেয়ে সামান্য বেশি, কারণ +1 পদটি)। এটাই N² বনাম N log N-এর মৌলিক পার্থক্য — N² বৃদ্ধিতে অনুপাত ৪-এর কাছাকাছি থাকে, কিন্তু N log N বৃদ্ধিতে অনুপাত ২-এর কাছাকাছি থাকে। L28-এ আমরা FFT-এর জন্য এই অনুপাত সত্যিই কম্পিউট করে দেখব।

প্র ০৩ কোডে কেন আমরা ওয়াল-ক্লক টাইমিং (যেমন time.time()) মাপিনি, বরং অপারেশন গুনেছি?

ওয়াল-ক্লক টাইমিং হার্ডওয়্যার, ব্রাউজার/Pyodide sandbox-এর লোড, ক্যাশিং ইত্যাদি অনেক অনিয়ন্ত্রিত বিষয়ের উপর নির্ভর করে — একই কোড দুইবার রান করলেও ভিন্ন সময় দেখাতে পারে, তাই এটি একটি অ্যালগরিদমের প্রকৃত কমপ্লেক্সিটি সম্পর্কে নির্ভরযোগ্য সিদ্ধান্ত দেয় না। অপারেশন কাউন্ট (কতগুলো গুণন/যোগ সত্যিই ঘটল) হার্ডওয়্যার-স্বাধীন এবং পুনরুৎপাদনযোগ্য — তাই এটিই একটি অ্যালগরিদমের সত্যিকারের কমপ্লেক্সিটি বিশ্লেষণের সঠিক উপায়।

অনুশীলন

  1. চিন্তা করুন: উপরের কোডে [8, 16, 32, 64, 128]-এর তালিকায় 256 যোগ করলে মোট অপারেশন সংখ্যা (2N² - N সূত্র ব্যবহার করে) কত হবে বলে আপনার ধারণা? নিজে হিসেব করার চেষ্টা করুন।

    2 × 256² - 256 = 2 × 65536 - 256 = 131072 - 256 = 130816টি মোট অপারেশন। এটি N=128-এর ৩২৬৪০ থেকে 130816/32640 ≈ 4.01x — অর্থাৎ প্যাটার্ন অনুযায়ী প্রায় ৪ গুণ বেড়েছে, যেমনটা প্রত্যাশিত।

  2. পরীক্ষা করুন: উপরের কোড সেলে তালিকায় 256 যোগ করে Run চেপে আপনার হিসেব যাচাই করুন।

    রান করলে সত্যিই 65536টি গুণন, 65280টি যোগ, এবং মোট ১,৩০,৮১৬টি অপারেশন দেখাবে — আগের ধাপের তুলনায় অনুপাত প্রায় ৪.০১x, যা প্রশ্ন ১-এর হাতে-করা হিসেবের সাথে মিলে যায়।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
জিরো-প্যাডিং ও ফ্রিকোয়েন্সি রেজোলিউশন