সরাসরি পর্বে যান
পর্ব ১৩ · ৭৯-এর মধ্যে · মডিউল ৩

NumPy array — shape, dtype আর list-এর চেয়ে কেন দ্রুত

NumPy Arrays — Shape, dtype and Why They Beat Python Lists

Taught in Bangla, this lesson opens up the NumPy ndarray: its contiguous buffer, shape, strides and dtype, how it differs from a Python list, creating arrays, memory arithmetic, type promotion and silent small-integer overflow, reshape versus transpose, and why vectorised code is faster, measured in the browser and in a real desktop run. It ends with the NumPy 2 changes that break old code.

এই পর্বে যা শিখবেন

  • একটি NumPy array মেমরিতে আসলে কী — buffer, shape, strides আর dtype — আর Python list থেকে তার পার্থক্য
  • np.array, zeros, ones, full, arange, linspace, eye দিয়ে array বানানো, আর shape, ndim, size, itemsize, nbytes হিসাব করা
  • কোন কাজে কোন dtype, মিশ্র type-এ upcasting, আর ছোট পূর্ণসংখ্যার নীরব overflow ধরা ও ঠেকানো
  • reshape, ravel, flatten আর transpose — buffer না বদলে ডেটাকে নতুনভাবে দেখা
  • Vectorised হিসাব কেন দ্রুত, তা নিজে মাপা; আর NumPy 2-এর যে বদলগুলো পুরোনো কোড আর AI সহকারীর কোড ভাঙে

১ · বাজারঘরের বিক্রির খাতা — list থেকে NumPy array

বাজারঘর (এই কোর্সের কাল্পনিক অনলাইন দোকান) প্রতিদিন চারটি জেলায় পণ্য পাঠায়: ঢাকা, চট্টগ্রাম, খুলনা আর রাজশাহী। হিসাব বিভাগ প্রতিটি জেলার প্রতিদিনের বিক্রি হাজার টাকায় লিখে রাখে। এক সপ্তাহে তাই একটি ছক তৈরি হয়: ৪টি সারি (জেলা) আর ৭টি কলাম (শনিবার থেকে শুক্রবার)। এই পুরো মডিউলে এই ছকটিই আমাদের সঙ্গী; পরের পর্বগুলোতে এর ভেতর থেকে বেছে নেওয়া, এর ওপর হিসাব করা আর এর ছবি আঁকা হবে।

Python-এ এমন ছক রাখার প্রথম উপায় list-এর ভেতরে list. পর্ব ০৯-এ দেখেছেন, এটি কাজ করে, কিন্তু লাখ লাখ সংখ্যায় ধীর হয়ে যায়। সেখানে একটি মাপও নিয়েছিলেন। এই পর্বে প্রশ্নটি আরও গভীরে নেওয়া হবে: NumPy-র ndarray (n-dimensional array, অর্থাৎ যত খুশি মাত্রার array) মেমরিতে ঠিক কী রাখে, আর সেখান থেকেই কেন গতি আসে।

দুটি দোকানের হিসাব রাখার ধরন ভাবুন। প্রথম দোকানি প্রতিদিনের বিক্রি একেকটি আলাদা চিরকুটে লেখেন, চিরকুটগুলো রাখেন দোকানের বিভিন্ন তাকে, আর একটি খাতায় শুধু লিখে রাখেন “শনিবারের চিরকুট তিন নম্বর তাকে”। প্রতিটি চিরকুটে আবার লেখা থাকে “এটি টাকার হিসাব”। দ্বিতীয় দোকানি ছক-কাটা একটি খাতা রাখেন: প্রতিটি ঘর সমান মাপের, ঘরগুলো পরপর, আর খাতার মলাটে একবার লেখা “৪ সারি, ৭ কলাম, সব হাজার টাকায়”। সপ্তাহের যোগফল বের করতে দ্বিতীয় দোকানিকে শুধু আঙুল চালিয়ে যেতে হয়; প্রথমজনকে তাকে তাকে ঘুরতে হয়। Python list প্রথম দোকানি, NumPy array দ্বিতীয়জন।

নিচের ছবিতে মেমরির এই দুই রূপ পাশাপাশি। ওপরে Python list: প্রতিটি ঘরে শুধু একটি ঠিকানা (pointer), আর আসল সংখ্যাটি মেমরির অন্য কোথাও একটি পূর্ণ Python int object. এমন object-কে বলে boxed object — সংখ্যাটি একটি “বাক্সে” মোড়া, বাক্সের গায়ে type, কতজন ব্যবহার করছে তার হিসাব (reference count) আর আকারের তথ্য। নিচে ndarray: একটি ছোট header, আর একটি টানা buffer-এ ৮ বাইট করে কাঁচা সংখ্যা।

Python list বনাম ndarray — মেমরির ভেতরে

A list stores pointers to boxed int objects; an ndarray stores metadata plus one contiguous typed buffer

Python list আর NumPy ndarray-এর মেমরির গঠন Python list: ঠিকানার সারি ঠিকানাঠিকানাঠিকানাঠিকানা refcounttype: int26 refcounttype: int74 refcounttype: int65 refcounttype: int50 প্রতিটি object ছড়ানো, প্রায় ২৮ বাইট (৬৪-বিট) ndarray: header + টানা buffer header (metadata) dtype: int64 (৮ বাইট) shape: (4, 7) strides: (56, 8) data → 26 74 65 50 50 80 26 081624324048 ঢাকার ৭ দিন, byte offset; পরের জেলা শুরু 56-এ মোট 28 × 8 = 224 বাইট, এক টানা
ছবি ১৩.১ — ওপরে list: প্রতিটি সংখ্যা একটি আলাদা Python object, তার গায়ে নিজের type লেখা। নিচে ndarray: type লেখা থাকে header-এ একবারই, আর ২৮টি সংখ্যা একটির পর একটি ৮ বাইট করে বসানো। Header-এর strides বলে দেয় পরের দিনে যেতে ৮ বাইট আর পরের জেলায় যেতে ৫৬ বাইট সরতে হবে।
মূল ধারণা

একটি ndarray দুই ভাগে তৈরি: (১) একটি contiguous বা টানা মেমরির টুকরো, যেখানে একই dtype-এর সংখ্যা পাশাপাশি রাখা; (২) সেই টুকরোকে কীভাবে পড়তে হবে তার বর্ণনা, অর্থাৎ metadata: dtype (প্রতিটি ঘর কত বাইট আর কী ধরনের সংখ্যা), shape (প্রতি মাত্রায় কতগুলো), strides (পরের ঘরে যেতে কত বাইট সরতে হবে)। এই পর্বের বাকি সবকিছু এই দুই ভাগের খেলা।

Python-এ array-র ইতিহাস কয়েক ধাপের। ১৯৯৫ সালের দিকে Jim Hugunin-সহ কয়েকজন “Numeric” নামের একটি package তৈরি করেন। পরে Space Telescope Science Institute-এর দল বড় ডেটার জন্য “Numarray” বানায়, আর বৈজ্ঞানিক Python সম্প্রদায় কিছুদিন দুই ভাগে বিভক্ত থাকে। ২০০৫–২০০৬ সালে Travis Oliphant দুটির ভালো দিক মিলিয়ে NumPy তৈরি করেন; NumPy 1.0 আসে ২০০৬ সালে। ২০২০ সালে Charles Harris ও সহকর্মীরা “Array programming with NumPy” নামে Nature পত্রিকায় একটি প্রবন্ধে NumPy-র নকশা আর প্রভাব বর্ণনা করেন। আর ২০২৪ সালের জুনে আসে NumPy 2.0, প্রায় দেড় যুগ পরে প্রথম বড় সংস্করণ-বদল (অংশ ৯-এ বিস্তারিত)।

২ · ndarray-এর ভেতরে — buffer, shape, strides আর dtype

বাজারঘরের সপ্তাহের ছকটিকে NumPy কীভাবে রাখে, এবার সংখ্যা ধরে দেখা যাক। ছকে ৪ × ৭ = ২৮টি সংখ্যা, প্রতিটি int64, অর্থাৎ ৬৪ বিট = ৮ বাইট। তাই buffer-এর আকার ২৮ × ৮ = ২২৪ বাইট। সংখ্যাগুলো সারি ধরে বসানো: আগে ঢাকার ৭ দিন, তারপর চট্টগ্রামের ৭ দিন, এভাবে। এই সাজানোর নাম row-major বা C order — শেষ মাত্রাটি (দিন) সবচেয়ে দ্রুত বদলায়।

এখন প্রশ্ন: “খুলনার মঙ্গলবারের বিক্রি” buffer-এর কোথায়? এখানেই strides-এর কাজ। Strides হলো প্রতিটি মাত্রায় এক ধাপ এগোতে কত বাইট সরতে হয় তার তালিকা। আমাদের ছকে পরের দিনে যেতে এক ঘর, অর্থাৎ ৮ বাইট; পরের জেলায় যেতে পুরো একটি সারি, অর্থাৎ ৭ × ৮ = ৫৬ বাইট। তাই strides = (56, 8). যেকোনো ঘরের ঠিকানা:

offset = i × s0 + j × s1

এখানে i সারির নম্বর (জেলা, ০ থেকে শুরু), j কলামের নম্বর (দিন), s0 = ৫৬ আর s1 = ৮ বাইট। Offset মানে buffer-এর শুরু থেকে কত বাইট দূরে। নিচের stepper-এ একই সূত্র কয়েকটি ঘরে চালিয়ে দেখুন — শেষ ধাপে একটি চমক আছে।

sales: shape (4, 7), int64strides (56, 8)
        শনি রবি সোম মঙ্গল বুধ বৃহ শুক্র
ঢাকা     26  74  65  50  50  80  26
চট্টগ্রাম  68  34  26  56  88  71  73
খুলনা    70  75  55  28  78  51  55
রাজশাহী   45  32  84  74  65  48  77
buffer: 28টি ঘর × 8 বাইট = 224 বাইট, সারি ধরে টানা
sales[0, 1]পরের দিন
offset = 0 × 56 + 1 × 8 = 8 বাইট
ঘর নম্বর = 8 ÷ 8 = 1
মান = 74   # ঢাকা, রবিবার
sales[1, 0]পরের জেলা
offset = 1 × 56 + 0 × 8 = 56 বাইট
ঘর নম্বর = 56 ÷ 8 = 7   # ঢাকার ৭টি ঘর পেরিয়ে
মান = 68   # চট্টগ্রাম, শনিবার
sales[2, 3]খুলনা, মঙ্গলবার
offset = 2 × 56 + 3 × 8 = 112 + 24 = 136 বাইট
ঘর নম্বর = 136 ÷ 8 = 17
মান = 28
sales.T: shape (7, 4)strides (8, 56)
sales.T[3, 2]   # মঙ্গলবার, খুলনা
offset = 3 × 8 + 2 × 56 = 24 + 112 = 136 বাইট
মান = 28   # একই ঘর, একই buffer
কিছুই কপি হয়নি, শুধু strides উল্টেছে
  1. ছক আর তার বর্ণনা। Seed ৪২ দিয়ে বানানো বাজারঘরের সপ্তাহ (হাজার টাকায়)। Shape (4, 7), dtype int64, তাই buffer ২২৪ বাইট। Strides (56, 8): পরের জেলায় ৫৬ বাইট, পরের দিনে ৮ বাইট।
  2. এক কলাম ডানে। ঢাকার রবিবার মানে i = 0, j = 1. Offset ০ × ৫৬ + ১ × ৮ = ৮ বাইট, অর্থাৎ buffer-এর ১ নম্বর ঘর, মান ৭৪। কলাম ধরে এগোলে পাশের ঘরেই পৌঁছানো যায়।
  3. এক সারি নিচে। চট্টগ্রামের শনিবার: i = 1, j = 0. Offset ৫৬ বাইট, মানে ৭টি ঘর লাফ দিয়ে ৭ নম্বর ঘর, মান ৬৮। সারি ধরে নিচে নামলে প্রতিবার একটি বড় লাফ।
  4. যেকোনো ঘর এক হিসাবে। খুলনার মঙ্গলবার: ২ × ৫৬ + ৩ × ৮ = ১৩৬ বাইট, ১৭ নম্বর ঘর, মান ২৮। কোনো খোঁজাখুঁজি নেই — একটি গুণ আর যোগেই ঠিকানা মেলে।
  5. Transpose মানে strides উল্টানো। sales.T-এর shape (7, 4) আর strides (8, 56). দিন ৩, জেলা ২: ৩ × ৮ + ২ × ৫৬ = ১৩৬ বাইট — একই ঘর, একই মান ২৮। নতুন ছক দেখালেও buffer একটিই; NumPy শুধু পড়ার নিয়ম বদলেছে।

এই কারণেই NumPy-তে অনেক কাজ প্রায় বিনা খরচে হয়: shape বা strides বদলানো মানে header-এর কয়েকটি সংখ্যা বদলানো, ২২৪ বাইট বা ২২৪ মেগাবাইট ডেটা নয়। যে নতুন array একই buffer ভাগ করে নেয়, তাকে বলে view; view আর copy-র পূর্ণ নিয়ম, আর কখন একটি view বদলালে মূল array-ও বদলে যায়, তা পর্ব ১৪-এ (Indexing আর slicing) আসবে।

একটি সতর্কতা। Strides বাইটে মাপা, ঘরে নয়। একই shape (4, 7)-এর array int32 হলে strides হবে (28, 4), float32 হলেও তাই, আর int8 হলে (7, 1). তাই কোনো আউটপুটে strides দেখলে আগে dtype দেখুন।

৩ · Array বানানোর পথ — np.array থেকে eye

ndarray বানানোর কয়েকটি পরিচিত পথ আছে। প্রতিটির নিজস্ব জায়গা — কোনোটি আগে থেকে থাকা ডেটা থেকে, কোনোটি খালি ছক তৈরির জন্য, কোনোটি সংখ্যার ধারা বানাতে।

Functionকী বানায়বাজারঘরে কোথায়
np.array(list)list বা list-এর list থেকে array; dtype নিজে অনুমান করেCSV থেকে পড়া কয়েকটি সারি
np.zeros(shape)সব ঘরে ০, default float64নতুন সপ্তাহের খালি ছক
np.ones(shape)সব ঘরে ১প্রতিটি জেলার জন্য সমান ভার
np.full(shape, v)সব ঘরে একই মান vডেলিভারি চার্জ সব জেলায় ৬০ টাকা
np.arange(start, stop, step)সমান ধাপে সংখ্যা, stop বাদ দিয়েদিনের নম্বর ০ থেকে ৬
np.linspace(a, b, n)a থেকে b পর্যন্ত ঠিক nটি সমান দূরত্বের সংখ্যা, দুই প্রান্তসহছাড়ের হার ০ থেকে ২০% পর্যন্ত ৫টি ধাপ
np.eye(n)n × n একক matrix — কর্ণে ১, বাকি ০পর্ব ১৭-এর linear algebra-য়

arange আর linspace-এর পার্থক্য মনে রাখার মতো। arange ধাপ ধরে চলে আর শেষ মান বাদ দেয়, Python-এর range-এর মতো। কিন্তু float ধাপে floating-point-এর ক্ষুদ্র ভুলের কারণে কয়টি সংখ্যা আসবে তা নিশ্চিত নয়: np.arange(0.1, 0.4, 0.1) চারটি সংখ্যা দেয় [0.1 0.2 0.3 0.4], যদিও ০.৪ বাদ পড়ার কথা (Python 3.14.4 আর NumPy 2.5.4-এ নিজে চালিয়ে দেখা)। NumPy-র docs-ও তাই বলে: অপূর্ণসংখ্যার ধাপে linspace ব্যবহার করুন, কারণ সেখানে সংখ্যা কয়টি তা আপনি নিজে বলে দেন।

এবার বাজারঘরের সপ্তাহের ছকটি বানানো যাক। rng.integers(20, 90, size=(4, 7)) ২০ থেকে ৮৯-এর মধ্যে (৯০ বাদ) এলোমেলো পূর্ণসংখ্যা দেয়, seed ৪২ থাকায় প্রত্যেক পাঠক একই সংখ্যা পাবেন। প্রথমবার “চালান” চাপলে ব্রাউজারে Python আর NumPy লোড হতে কয়েক সেকেন্ড লাগতে পারে।

import numpy as np

rng = np.random.default_rng(42)
# Bazarghar: 4 districts x 7 days, sales in thousand taka
sales = rng.integers(20, 90, size=(4, 7))
print(sales)
print("shape:", sales.shape, "ndim:", sales.ndim, "size:", sales.size)
print("dtype:", sales.dtype, "itemsize:", sales.itemsize,
      "nbytes:", sales.nbytes, "strides:", sales.strides)

week = np.array([[26, 74, 65], [68, 34, 26]])   # from a list of lists
print("from lists ->", week.dtype, week.shape)
print("zeros:", np.zeros(3), np.zeros(3).dtype)
print("ones int8:", np.ones(3, dtype=np.int8))
print("full:", np.full((2, 2), 60))
print("arange:", np.arange(7))
print("linspace:", np.linspace(0, 0.2, 5))
print(np.eye(3, dtype=np.int64))

ব্রাউজারে (Pyodide 314.0.7, NumPy 2.4.6) আসল আউটপুট:

[[26 74 65 50 50 80 26]
 [68 34 26 56 88 71 73]
 [70 75 55 28 78 51 55]
 [45 32 84 74 65 48 77]]
shape: (4, 7) ndim: 2 size: 28
dtype: int64 itemsize: 8 nbytes: 224 strides: (56, 8)
from lists -> int32 (2, 3)
zeros: [0. 0. 0.] float64
ones int8: [1 1 1]
full: [[60 60]
 [60 60]]
arange: [0 1 2 3 4 5 6]
linspace: [0.   0.05 0.1  0.15 0.2 ]
[[1 0 0]
 [0 1 0]
 [0 0 1]]

প্রথম চার সারি বাজারঘরের সপ্তাহ: ঢাকার শনিবার ২৬ হাজার টাকা, শুক্রবারও ২৬; চট্টগ্রামের বুধবার ৮৮, সপ্তাহের সবচেয়ে বড়। পরের দুই লাইন অংশ ২-এর সংখ্যাগুলোই নিশ্চিত করে: shape (4, 7), ২ মাত্রা, ২৮টি ঘর, প্রতিটি ৮ বাইট, মোট ২২৪ বাইট, strides (56, 8). লক্ষ করুন week: list-এর list থেকে বানানো, dtype না বলায় ব্রাউজারে এসেছে int32 — এটি ব্রাউজারের ৩২-বিট Python-এর বৈশিষ্ট্য, অংশ ৫-এ এর ব্যাখ্যা। zeros-এর ফল [0. 0. 0.] — শেষে বিন্দু মানে float64, NumPy-র default float. linspace(0, 0.2, 5) দুই প্রান্তসহ ঠিক ৫টি ছাড়ের হার দিয়েছে: ০%, ৫%, ১০%, ১৫% আর ২০% — প্রতিটি ধাপে ৫ শতাংশ। আর eye(3) কর্ণে ১ বসানো ৩ × ৩ একক matrix.

৪ · shape, ndim, size, itemsize, nbytes — মেমরির হিসাব

পাঁচটি attribute প্রতিদিন কাজে লাগে, আর এদের মধ্যে সরল সম্পর্ক আছে:

  • shape: প্রতি মাত্রায় কতগুলো ঘর, একটি tuple. বিক্রির ছকে (4, 7).
  • ndim: মাত্রার সংখ্যা = shape-এর দৈর্ঘ্য। এখানে ২। একটি রঙিন ছবি সাধারণত ৩ মাত্রার (উচ্চতা, প্রস্থ, রং), পর্ব ১৮-এ দেখবেন।
  • size: মোট ঘর = shape-এর সংখ্যাগুলোর গুণফল। ৪ × ৭ = ২৮।
  • itemsize: একটি ঘর কত বাইট — dtype থেকেই আসে। int64-এ ৮।
  • nbytes: buffer-এর মোট আকার = size × itemsize = ২৮ × ৮ = ২২৪।

পরীক্ষায় আর বাস্তবে এই হিসাব প্রায়ই লাগে। ধরুন বাজারঘর একদিন ৬৪টি জেলাতেই পণ্য পাঠাবে, আর এক বছরের (৩৬৫ দিন) দৈনিক বিক্রি রাখতে চায়:

Arraysizeitemsizenbytes
৬৪ জেলা × ৩৬৫ দিন, int64২৩,৩৬০৮১,৮৬,৮৮০ বাইট ≈ ১৮৭ KB
একই, float32২৩,৩৬০৪৯৩,৪৪০ বাইট
একটি Full HD রঙিন ছবি, (1080, 1920, 3), uint8৬২,২০,৮০০১৬২,২০,৮০০ বাইট ≈ ৫.৯ MiB
একই ছবি float64-এ৬২,২০,৮০০৮প্রায় ৪৭.৫ MiB — আট গুণ

শেষ সারিটি একটি বাস্তব শিক্ষা: ছবিকে ০ থেকে ১-এর float-এ নিলে মেমরি আট গুণ হয়ে যায়। অনেক deep learning কাজে তাই float32 ব্যবহার হয় (চার গুণ)। একটি সূক্ষ্ম কথা: nbytes শুধু buffer-এর হিসাব, header-এর শ-খানেক বাইট এতে নেই; আর কোনো view-এর nbytes তার নিজের ঘরগুলোর হিসাব দেয়, যদিও buffer অন্যের সঙ্গে ভাগ করা। মেমরি মাপার পূর্ণ খুঁটিনাটি পর্ব ১৯-এ (NumPy-র গতি)।

৫ · dtype বাস্তবে — int8 থেকে float64, bool আর platform-এর default

dtype (data type) বলে দেয় buffer-এর প্রতিটি ঘর কত বাইটের আর বিটগুলোকে কী হিসেবে পড়তে হবে: চিহ্নসহ পূর্ণসংখ্যা, চিহ্নহীন পূর্ণসংখ্যা, দশমিক সংখ্যা, নাকি সত্য-মিথ্যা। একই ৮টি বিট 10111000 uint8 হিসেবে পড়লে ১৮৪, int8 হিসেবে পড়লে −৭২ — অংশ ৬-এ এর কারণ দেখবেন। Python-এর নিজের int যত বড় খুশি হতে পারে; NumPy-র int-এর আকার বাঁধা।

dtypeবাইটসীমা বা নির্ভুলতাসাধারণ ব্যবহার
bool১True / Falseশর্তের ফল, mask (পর্ব ১৪)
int8১−১২৮ থেকে ১২৭খুব ছোট গণনা, rating ১–৫
uint8১০ থেকে ২৫৫ছবির pixel (পর্ব ১৮)
int16২−৩২,৭৬৮ থেকে ৩২,৭৬৭অডিওর নমুনা
int32৪প্রায় ±২১৪ কোটি (২,১৪৭,৪৮৩,৬৪৭)id, গণনা
int64৮প্রায় ±৯.২ × ১০১৮টাকা পয়সায়, সময়ের হিসাব
float32৪প্রায় ৭টি অর্থবহ অঙ্কneural network, GPU
float64৮প্রায় ১৫–১৬টি অর্থবহ অঙ্কNumPy-র default float, পরিসংখ্যান

float32 নাকি float64? float64-এর নির্ভুলতা অনেক বেশি, তাই পরিসংখ্যান, অর্থের হিসাব আর scikit-learn-এর বেশিরভাগ কাজে এটিই স্বাভাবিক পছন্দ। float32 অর্ধেক মেমরি নেয়, আর GPU-তে সাধারণত অনেক দ্রুত, তাই deep learning-এ এটি প্রচলিত (আরও ছোট float16 বা bfloat16-ও ব্যবহার হয়, ভাবনার প্রশ্ন ৩ দেখুন)। কিন্তু float32-এ ১,৬৭,৭৭,২১৬ (২২৪)-এর বেশি প্রতিটি পূর্ণসংখ্যা ঠিকঠাক রাখা যায় না, তাই বড় টাকার যোগফল বা id কখনো float32-এ নয়। টাকার হিসাবে সবচেয়ে নিরাপদ হলো পয়সায় int64, যা পর্ব ০৯-এ দেখেছেন।

Default int platform-এর ওপর নির্ভর করে। dtype না বললে NumPy পূর্ণসংখ্যার জন্য “platform-এর default int” নেয়। NumPy 2.0 থেকে সব ৬৪-বিট যন্ত্রে, Windows-সহ, এটি int64; আগে Windows-এ ছিল int32, যা অনেক বাগের উৎস ছিল বলে NumPy-র release notes নিজেই বলেছে। কিন্তু এই কোর্সের ব্রাউজারের Python (Pyodide) ৩২-বিট WebAssembly-তে চলে, তাই সেখানে default int হলো int32. ওপরের কোডে week array-টি তাই ব্রাউজারে int32 দেখিয়েছে, আর আপনার ল্যাপটপে একই লাইন দেবে int64. অন্যদিকে rng.integers নিজের default int64 সব জায়গায় একই রাখে।

যেখানে আকার বা overflow গুরুত্বপূর্ণ (টাকার যোগফল, বড় গণনা, ফাইলে লিখে অন্য যন্ত্রে পড়া), সেখানে dtype নিজে লিখুন: np.array(values, dtype=np.int64). “আমার ল্যাপটপে তো চলেছে” — dtype না লিখলে এই বাক্যটি অন্য যন্ত্রে মিথ্যা হতে পারে।

লেখা (string) নিয়েও একটি কথা। np.array(["Dhaka", "Khulna"])-এর dtype <U6 — প্রতিটি ঘরে ঠিক ৬টি Unicode অক্ষরের জায়গা, যা সবচেয়ে লম্বা লেখার মাপে বাঁধা। পরে লম্বা নাম বসালে চুপচাপ কেটে যায়। NumPy-র আসল শক্তি সংখ্যায়; লেখাভরা টেবিলের জন্য pandas (মডিউল ৪) বেশি উপযোগী।

৬ · মিশ্র type-এ upcasting আর ছোট int-এর নীরব overflow

দুটি ভিন্ন dtype-এর array দিয়ে হিসাব করলে NumPy-কে ফলের dtype ঠিক করতে হয়। নিয়মটির নাম type promotion, আর ছোট type থেকে বড় type-এ ওঠাকে বলে upcasting. মূল নীতি: এমন একটি dtype বেছে নেওয়া হয়, যা দুই পক্ষের সব মান ধারণ করতে পারে। int8 + int16 → int16; পূর্ণসংখ্যা + দশমিক → দশমিক; bool + int → int. List থেকে বানানোর সময়ও একই নিয়ম: np.array([1, 2.5, True]) দেয় float64, আর একটি লেখা ঢুকে গেলে পুরো array-ই লেখা হয়ে যায়।

সবচেয়ে অবাক করা নিয়মটি ধাপে ধাপে দেখুন: int32-এর বিক্রির একক আর float32-এর দাম গুণ করলে ফল float32 নয়, float64.

1units = np.array([3, 5], dtype=np.int32)2price = np.array([12.5, 8.0], dtype=np.float32)3kind = np.result_type(units, price)4total = units * price5print((price * 2.5).dtype)
মানগুলো (Watch)
units
price
kind
total
  1. বিক্রির একক। দুটি পণ্যের বিক্রি ৩ আর ৫ পিস, dtype int32 — প্রতিটি ৪ বাইট, সীমা প্রায় ±২১৪ কোটি।
  2. দাম। দাম হাজার টাকায় ১২.৫ আর ৮.০, dtype float32 — এটিও ৪ বাইট, কিন্তু অর্থবহ অঙ্ক প্রায় ৭টি।
  3. ফলের dtype ঠিক করা। float32 কি সব int32 মান ঠিকঠাক রাখতে পারে? না — float32-এ ২২৪ = ১,৬৭,৭৭,২১৬-এর ওপরের সব পূর্ণসংখ্যা ধরা যায় না। তাই NumPy উঠে যায় float64-এ, যার ৫৩ বিটের অংশে সব int32 অনায়াসে ধরে। np.result_type ঠিক এই সিদ্ধান্তটি দেখায়।
  4. গুণ। দুই array float64-এ রূপান্তরিত হয়ে গুণ হয়: ৩ × ১২.৫ = ৩৭.৫, ৫ × ৮ = ৪০। ফলের প্রতিটি ঘর এখন ৮ বাইট — মেমরি দ্বিগুণ, যা বড় ডেটায় টের পাওয়া যায়।
  5. Python সংখ্যা “দুর্বল”। price * 2.5-এ ২.৫ একটি সাধারণ Python float. NumPy 2-এর নিয়মে (NEP 50) এমন Python সংখ্যা array-র dtype মেনে নেয়, তাই ফল float32-ই থাকে। ফলের dtype ঠিক হয় array-র dtype দেখে, ভেতরের মান দেখে নয়।

এবার উল্টো দিকের বিপদ। ছোট dtype-এর array-র হিসাবে ফল সীমা পেরোলে NumPy ফলকে বড় type-এ তোলে না — সংখ্যাটি চুপচাপ উল্টো প্রান্তে ঘুরে যায়। একে বলে integer overflow. Python-এর নিজের int-এ এটা কখনো হয় না, তাই Python থেকে আসা মানুষ প্রায়ই অবাক হন। খুলনার এক সপ্তাহের অর্ডার-সংখ্যা কেউ “মেমরি বাঁচাতে” int8-এ রেখেছিলেন; ঈদের সপ্তাহে অর্ডার দ্বিগুণ হলো:

import numpy as np

print("int8 range:", np.iinfo(np.int8).min, "to", np.iinfo(np.int8).max)
# Khulna: orders per day for one week, stored "to save memory"
orders = np.array([92, 86, 87, 73, 65, 93, 113], dtype=np.int8)
eid = orders * 2                        # Eid week: orders double
print("orders    :", orders)
print("orders * 2:", eid, eid.dtype)    # silent wrap-around, no warning
print("92 * 2 = 184 -> 184 - 256 =", 184 - 256)

total = orders.sum()                    # sum widens the accumulator
print("orders.sum():", total, "| dtype:", total.dtype)
try:
    orders + 200                        # 200 itself does not fit in int8
except OverflowError as e:
    print("OverflowError:", e)

safe = orders.astype(np.int64) * 2      # widen first, then multiply
print("safe      :", safe, safe.dtype)
print("int8 * 1.5 ->", (orders * 1.5).dtype)

ব্রাউজারে আসল আউটপুট:

int8 range: -128 to 127
orders    : [ 92  86  87  73  65  93 113]
orders * 2: [ -72  -84  -82 -110 -126  -70  -30] int8
92 * 2 = 184 -> 184 - 256 = -72
orders.sum(): 609 | dtype: int32
OverflowError: Python integer 200 out of bounds for int8
safe      : [184 172 174 146 130 186 226] int64
int8 * 1.5 -> float64

সাতটি অর্ডার-সংখ্যাই ১২৭-এর নিচে, তাই int8-এ ঠিকঠাক বসেছে। দ্বিগুণ করতেই সব ভেঙে গেল: ১৮৪ হয়ে গেল −৭২, ২২৬ হয়ে গেল −৩০ — আর কোনো error বা warning নেই, dtype এখনো int8. ঋণাত্মক অর্ডার-সংখ্যা দেখলে হয়তো চোখে পড়ত, কিন্তু অন্য হিসাবে ভুল ফল ধনাত্মকও আসতে পারে: তিন গুণ করলে ৯২ × ৩ = ২৭৬ হয়ে যায় ২০, দেখতে একদম স্বাভাবিক। sum() কিন্তু ঠিক ৬০৯ দিয়েছে: NumPy ছোট int-এর যোগফলের জন্য নিজে থেকেই platform-এর default int-এ যোগ করে — ব্রাউজারে তাই dtype int32, ল্যাপটপে আসবে int64. কিন্তু উপাদান-ধরে গুণ বা যোগে (*, +) এমন সুরক্ষা নেই। orders + 200-এ NumPy 2 সরাসরি OverflowError দিয়েছে, কারণ ২০০ সংখ্যাটি নিজেই int8-এ ধরে না (NEP 50-এর নিয়ম; অংশ ৯ দেখুন)। সমাধান সহজ: আগে astype(np.int64), তারপর হিসাব — ঈদের সপ্তাহের ঠিক সংখ্যাগুলো ১৮৪ থেকে ২২৬। শেষ লাইন: int8-এর সঙ্গে একটি Python float মিশলে ফল দশমিকে ওঠে, float64.

কেন ১৮৪ হয়ে যায় −৭২? NumPy-র চিহ্নসহ পূর্ণসংখ্যা two's complement পদ্ধতিতে রাখা হয়: ৮ বিটে সর্বোচ্চ বিটটির মান −১২৮, বাকি সাতটির ৬৪, ৩২, ১৬, ৮, ৪, ২, ১। ১৮৪-এর বিট 10111000; চিহ্নহীন পড়লে ১২৮ + ৩২ + ১৬ + ৮ = ১৮৪, কিন্তু চিহ্নসহ পড়লে −১২৮ + ৩২ + ১৬ + ৮ = −৭২। সহজ সূত্র: ফল ১২৭-এর বেশি হলে ২৫৬ বিয়োগ করুন। বিটের পূর্ণ গল্প আছে কম্পিউটার আর্কিটেকচার কোর্সের পূর্ণসংখ্যার প্রকাশ পর্বে।

মূল ধারণা

Array-র হিসাবে ছোট int-এর overflow কোনো error বা warning দেয় না (একক NumPy scalar-এ শুধু একটি RuntimeWarning আসে)। প্রতিকার তিনটি: (১) সন্দেহ থাকলে int64; (২) ছোট dtype-এ রাখা ডেটায় হিসাবের আগে astype(np.int64); (৩) np.iinfo(dtype) দিয়ে সীমা দেখে মানগুলো যাচাই। মেমরি বাঁচানো তখনই, যখন সীমা নিশ্চিতভাবে জানা — যেমন pixel-এর ০–২৫৫।

৭ · reshape, ravel, flatten আর transpose — একই ডেটা, নতুন চোখে

reshape মানে একই সংখ্যাগুলোকে অন্য shape-এ দেখা। শর্ত একটাই: মোট ঘরের সংখ্যা (size) বদলাতে পারবে না। ২৮টি সংখ্যা (2, 14), (14, 2), (4, 7) বা (28,) হতে পারে, কিন্তু (5, 6) নয়। একটি মাত্রায় -1 লিখলে NumPy নিজে হিসাব করে নেয়: reshape(-1) মানে “এক সারিতে সব”, reshape(2, -1) মানে “দুই সারি, কলাম যত লাগে”। Row-major ক্রমে সংখ্যাগুলো যে সারিতে buffer-এ আছে, সেই সারিতেই নতুন shape-এর ঘরগুলো ভরে। নিচে ছয়টি সংখ্যা দিয়ে দেখুন।

flat: shape (6,), int64strides (8,)
ঘর     0   1   2   3   4   5
মান    26  74  65  50  50  80
offset 0   8   16  24  32  40
# ঢাকার প্রথম ছয় দিনের বিক্রি
flat.reshape(2, 3)নতুন header
shape   (6,)  →  (2, 3)
strides (8,)  →  (24, 8)   # এক সারি = 3 × 8
buffer  একই, কিছু কপি হয়নি
সারি 0offset 0, 8, 16
[[26 74 65]
 [ .  .  . ]]
সারি 1শুরু offset 24-এ
[[26 74 65]
 [50 50 80]]
[1, 2] → 1 × 24 + 2 × 8 = 40 → 80
reshape(3, 2) বনাম .Tএক নয়!
flat.reshape(3, 2)        r.T  (r = reshape(2, 3))
[[26 74]                  [[26 50]
 [65 50]                   [74 50]
 [50 80]]                  [65 80]]
একই shape (3, 2), ভিন্ন সংখ্যা-বিন্যাস
  1. একটি টানা buffer. ছয়টি int64 সংখ্যা, প্রতিটি ৮ বাইট, offset ০ থেকে ৪০। Shape (6,), strides (8,).
  2. শুধু header বদলায়। reshape(2, 3) buffer ছোঁয় না। নতুন shape (2, 3), আর নতুন strides (24, 8): পরের সারিতে যেতে ৩টি ঘর = ২৪ বাইট।
  3. প্রথম সারি ভরে আগে। Row-major নিয়মে সারি ০ নেয় buffer-এর প্রথম তিনটি ঘর: ২৬, ৭৪, ৬৫।
  4. তারপর দ্বিতীয় সারি। সারি ১ শুরু হয় offset ২৪-এ: ৫০, ৫০, ৮০। ঘর [1, 2]-এর offset ১ × ২৪ + ২ × ৮ = ৪০, মান ৮০ — অংশ ২-এর সূত্রই।
  5. reshape আর transpose গুলিয়ে ফেলবেন না। reshape(3, 2) buffer-এর ক্রমে আবার কাটে: [২৬ ৭৪], [৬৫ ৫০], [৫০ ৮০]। .T সারি আর কলাম অদলবদল করে: [২৬ ৫০], [৭৪ ৫০], [৬৫ ৮০]। দুটোরই shape (3, 2), কিন্তু অর্থ সম্পূর্ণ আলাদা — এটি ডেটার কাজের খুব সাধারণ নীরব বাগ।

এক মাত্রায় নামানোর দুটি পথ: ravel() পারলে একটি view দেয় (কপি নয়), আর flatten() সবসময় নতুন কপি দেয়। তাই ravel দ্রুত আর মেমরি-সাশ্রয়ী, কিন্তু তার ফল বদলালে মূল array-ও বদলাতে পারে; flatten-এর ফল নিশ্চিন্তে বদলানো যায়। Transpose (.T) সারি আর কলাম অদলবদল করে — অংশ ২-এ দেখেছেন, এটি শুধু strides উল্টায়। বাজারঘরের ছকে তার মানে “জেলা × দিন” থেকে “দিন × জেলা”।

import numpy as np

rng = np.random.default_rng(42)
sales = rng.integers(20, 90, size=(4, 7))   # 4 districts x 7 days
print("sales         ", sales.shape, "strides", sales.strides)

t = sales.T                                 # 7 days x 4 districts
print("sales.T       ", t.shape, "strides", t.strides)
print("T shares memory:", np.shares_memory(sales, t))

two = sales.reshape(2, 14)                  # 28 numbers, 2 rows of 14
print("reshape(2, 14)", two.shape, "strides", two.strides)
print("reshape(-1)   ", sales.reshape(-1).shape)
print("reshape(2, -1)", sales.reshape(2, -1).shape)

r = sales.ravel()
f = sales.flatten()
print("ravel shares memory:  ", np.shares_memory(sales, r))
print("flatten shares memory:", np.shares_memory(sales, f))
print("sales.T.ravel()[:8]:", t.ravel()[:8])
print("its memory shared:  ", np.shares_memory(sales, t.ravel()))

try:
    sales.reshape(5, 6)
except ValueError as e:
    print("ValueError:", e)

ব্রাউজারে আসল আউটপুট:

sales          (4, 7) strides (56, 8)
sales.T        (7, 4) strides (8, 56)
T shares memory: True
reshape(2, 14) (2, 14) strides (112, 8)
reshape(-1)    (28,)
reshape(2, -1) (2, 14)
ravel shares memory:   True
flatten shares memory: False
sales.T.ravel()[:8]: [26 68 70 45 74 34 75 32]
its memory shared:   False
ValueError: cannot reshape array of size 28 into shape (5,6)

Transpose-এ shape (7, 4), strides উল্টে (8, 56), আর np.shares_memory বলছে buffer একই — অংশ ২-এর stepper-এর শেষ ধাপ। reshape(2, 14)-এ এক সারিতে ১৪টি ঘর, তাই সারির stride ১৪ × ৮ = ১১২ বাইট; -1 দুই ক্ষেত্রেই NumPy নিজে হিসাব করেছে (২৮ ÷ ২ = ১৪)। ravel মূল buffer ভাগ করেছে, flatten নতুন কপি দিয়েছে। পরের দুই লাইনে একটি সূক্ষ্ম জিনিস: transpose-এর ravel দিনের ক্রমে পড়ে — শনিবারে চার জেলা ২৬, ৬৮, ৭০, ৪৫, তারপর রবিবারে ৭৪, ৩৪, ৭৫, ৩২। এই ক্রম মূল buffer-এ টানা নেই, তাই এখানে ravel-কেও কপি করতে হয়েছে (shared: False). “ravel পারলে view দেয়” কথাটির “পারলে” অংশ এটাই। শেষে (5, 6): ৫ × ৬ = ৩০, অথচ ঘর ২৮ — NumPy পরিষ্কার ValueError দিয়েছে।

৮ · Array কেন দ্রুত — memory layout, Python-এর খরচ আর SIMD

“NumPy দ্রুত, কারণ C-তে লেখা” — কথাটি সত্য, কিন্তু অসম্পূর্ণ। গতি আসে তিনটি জিনিস একসঙ্গে থাকায়:

  1. প্রতি সংখ্যায় Python-এর খরচ নেই। Python loop-এর প্রতি ধাপে interpreter-কে object-এর type দেখতে হয়, গুণের নিয়ম খুঁজতে হয়, ফলের জন্য নতুন boxed object বানাতে হয়, পুরোনোটির reference count কমাতে হয়। NumPy dtype একবার দেখে, তারপর সেই dtype-এর জন্য লেখা একটি compiled C loop পুরো buffer-এর ওপর চালায়। এটিই vectorization-এর মূল কথা; broadcasting-সহ এর পূর্ণ রূপ পর্ব ১৫-এ।
  2. টানা মেমরি। CPU মেমরি থেকে একটি একটি বাইট আনে না, একসঙ্গে একটি ছোট খণ্ড (সাধারণত ৬৪ বাইটের cache line) আনে। টানা buffer-এ একটি খণ্ডেই আটটি int64 চলে আসে, আর পরের খণ্ডটি কী হবে তা CPU আগেই আন্দাজ করে আনতে পারে। List-এর ছড়ানো object-এ এই সুবিধা কম। বিস্তারিত আছে cache-এর মৌলিক ধারণা পর্বে।
  3. SIMD. আধুনিক CPU একটি instruction-এ একাধিক সংখ্যার ওপর একই কাজ করতে পারে — SIMD (Single Instruction, Multiple Data). NumPy-র অনেক মৌলিক loop যন্ত্রের SIMD সুবিধা ব্যবহারের জন্য লেখা। তবে কোন কাজে, কোন CPU-তে, কতটা লাভ — তা ভিন্ন হয়; আর ব্রাউজারের WebAssembly-তে এই সুবিধা ডেস্কটপের সমান হবে, এমন নিশ্চয়তা নেই। তাই SIMD-কে একটি সম্ভাব্য বাড়তি লাভ ধরুন, নিশ্চিত গুণফল নয়। দেখুন SIMD আর vector পর্ব।

এবার মাপা যাক। একই ২ লাখ বিক্রির সংখ্যা (টাকায়) একটি list-এ আর একটি array-তে; কাজ: প্রতিটিতে ৫% বাড়িয়ে মোট যোগফল। মেমরিও মাপা হবে sys.getsizeof দিয়ে। প্রতিটি মাপ ৫ বার নিয়ে সবচেয়ে কম সময়টি রাখা হয়েছে, কারণ অন্য কাজের বাধায় সময় কেবল বাড়তে পারে, কমতে নয়।

import sys
import time
import numpy as np

N = 200_000
rng = np.random.default_rng(42)
arr = rng.integers(20_000, 90_000, size=N)  # taka, dtype int64
lst = arr.tolist()                          # same numbers as Python ints

objs = sum(sys.getsizeof(x) for x in lst)
print("one Python int:", sys.getsizeof(lst[0]), "bytes")
print("list :", sys.getsizeof(lst), "B pointers +", objs, "B objects")
print("array:", arr.nbytes, "B data, dtype", arr.dtype)

def best_ms(f, repeat=5):
    times = []
    for _ in range(repeat):
        t0 = time.perf_counter()
        result = f()
        times.append(time.perf_counter() - t0)
    return min(times) * 1000, result

t_list, s1 = best_ms(lambda: sum(x * 105 // 100 for x in lst))
t_arr, s2 = best_ms(lambda: int((arr * 105 // 100).sum()))
print(f"list : {t_list:7.2f} ms, total = {s1:,}")
print(f"array: {t_arr:7.2f} ms, total = {s2:,}")
print("same total:", s1 == s2, "| time ratio:", round(t_list / t_arr))

ব্রাউজারে একটি run-এর আসল আউটপুট (আপনার পর্দায় সময় আলাদা হবে):

one Python int: 16 bytes
list : 800028 B pointers + 3200000 B objects
array: 1600000 B data, dtype int64
list :   16.00 ms, total = 11,557,184,712
array:    0.60 ms, total = 11,557,184,712
same total: True | time ratio: 27

দুই পথে মোট একই: ১১,৫৫৭,১৮৪,৭১২ টাকা। এই run-এ list প্রায় ১৬ মিলিসেকেন্ড, array প্রায় ০.৬ — প্রায় ২৭ গুণ। একই ব্রাউজারে পরের একটি run-এ এসেছিল ১৮.৫ আর ১.৫ মিলিসেকেন্ড, প্রায় ১২ গুণ — আপনার যন্ত্রে আর প্রতিবার চালানোয় সংখ্যাটি বদলাবে, তবে array প্রতিবারই অনেক এগিয়ে। মেমরির লাইনগুলো ব্রাউজারের ৩২-বিট Python-এর নিজস্ব: এখানে প্রতিটি ঠিকানা ৪ বাইট (২ লাখ × ৪ ≈ ৮ লাখ বাইট) আর একটি ছোট Python int ১৬ বাইট, তাই list-এ মোট প্রায় ৪০ লাখ বাইট, অর্থাৎ প্রতি সংখ্যায় ২০ বাইট। Array-তে আমরা int64 নিয়েছি, প্রতি সংখ্যায় ৮ বাইট, মোট ১৬ লাখ। ৬৪-বিট ল্যাপটপে ঠিকানা ৮ বাইট আর int object ২৮ বাইট, তাই সেখানে ব্যবধান আরও বড় — নিচের সত্যিকারের run দেখুন।

ব্রাউজার একটি বিশেষ পরিবেশ, তাই লেখকের ল্যাপটপে সাধারণ Python দিয়ে একই কাজ ১ কোটি সংখ্যায় চালানো হয়েছিল। সত্যিকারের run, একবার, Python 3.14.4 আর NumPy 2.5.4 (লেখার সময়ের সর্বশেষ স্থিতিশীল সংস্করণ):

~/bazarghar-ai$ python bench_big.py
Python 3.14.4, NumPy 2.5.4
N = 10,000,000
list :    80.0 MB pointers + 280.0 MB int objects
array:    80.0 MB data, dtype=int64
+5% then total, list :    373.5 ms  -> 577,443,171,617
+5% then total, array:     48.9 ms  -> 577,443,171,617
same answer: True, array faster by about 8x
float32 x1.05 sum:   15.3 ms (40 MB)
float64 x1.05 sum:   26.8 ms (80 MB)

কয়েকটি জিনিস লক্ষ করুন। মেমরি: list-এ ১ কোটি ঠিকানা (৮০ MB) আর ১ কোটি int object (প্রতিটি ২৮ বাইট, ২৮০ MB) — মোট প্রায় ৩৬০ MB; array-তে শুধু ৮০ MB. সাড়ে চার গুণ পার্থক্য। সময়ে এই একটি run-এ array প্রায় ৮ গুণ দ্রুত। এই অনুপাত ওপরের ব্রাউজার-মাপের (দুই run-এ প্রায় ২৭ আর ১২ গুণ) চেয়ে কম। একটি কারণ, ব্রাউজারে WebAssembly-তে চলা Python loop ল্যাপটপের চেয়ে ধীর, তাই সেখানে অনুপাত বাড়ে। আরেকটি কারণ, এখানে array অনেক বড় (৮০ MB), যা CPU-র cache-এ ধরে না, আর arr * 105 // 100 আরও দুটি পূর্ণ মধ্যবর্তী array তৈরি করে। তখন হিসাবের চেয়ে মেমরি থেকে ডেটা আনা-নেওয়াই বড় খরচ, যা loop-এর লাভের একটি অংশ খেয়ে ফেলে। Float-এর শেষ দুই লাইনে অর্ধেক আকারের float32 প্রায় অর্ধেক সময় নিয়েছে — বড় array-তে গতির বড় অংশ আসলে মেমরি থেকে ডেটা আনার খরচ। এগুলো একটি যন্ত্রের একটি run-এর সংখ্যা, সাধারণ নিয়ম নয়; কাজ, array-র আকার আর যন্ত্র বদলালে অনুপাতও বদলায়।

কখন array দ্রুত নয়। খুব ছোট array-তে (ধরুন ১০টি সংখ্যা) প্রতিটি NumPy call-এর নিজস্ব নির্দিষ্ট খরচ loop-এর লাভকে ছাপিয়ে যেতে পারে। প্রতিটি ধাপে আগের ফলের ওপর নির্ভর করে এমন হিসাব (যেমন একটি একটি করে অর্ডার ধরে মজুত কমানো, শর্তসহ) সহজে vectorise হয় না। আর np.append দিয়ে loop-এ একটি একটি করে বাড়ানো প্রতিবার পুরো array কপি করে — list-এ জমিয়ে শেষে একবার np.array করাই ভালো। কখন loop-ই ঠিক, তা পর্ব ১৯-এ।

৯ · NumPy 2-এ যা বদলেছে — নতুনদের জন্য জরুরি কয়েকটি

NumPy 2.0 আসে ২০২৪ সালের জুনে। বেশিরভাগ কোড আগের মতোই চলে, কিন্তু ইন্টারনেটের পুরোনো tutorial, Stack Overflow-এর উত্তর আর AI সহকারীর শেখা কোডে NumPy 1-এর অভ্যাস ছড়িয়ে আছে। নিচের বদলগুলো numpy.org-এর 2.0.0 release notes থেকে মিলিয়ে নেওয়া:

বদলপুরোনো কোডএখন লিখুন
কিছু alias সরানো হয়েছেnp.float_, np.complex_, np.NaN, np.Infnp.float64, np.complex128, np.nan, np.inf
আরও আগে (NumPy 1.24-এ) সরানোnp.float, np.int, np.objectPython-এর float, int, object, বা নির্দিষ্ট np.float64
Scalar-এর ছাপা রূপ (NEP 51)repr দেখাত 1.5এখন np.float64(1.5); সাধারণ লেখা চাইলে str() বা float()
Type promotion (NEP 50)ফলের dtype কখনো মানের ওপর নির্ভর করতশুধু dtype দেখে; Python সংখ্যা “দুর্বল”, সীমার বাইরে হলে OverflowError
Windows-এ default intint32int64, অন্য ৬৪-বিট যন্ত্রের মতো
নতুন লেখার dtypeনির্দিষ্ট দৈর্ঘ্যের <U বা objectইচ্ছা হলে np.dtypes.StringDType(), যত খুশি দৈর্ঘ্যের UTF-8 লেখা; default এখনো <U
np.bool১.২৪-এ সরানো হয়েছিলআবার আছে, এবার NumPy-র নিজের bool dtype-এর নাম

নিজের যন্ত্রে এগুলো যাচাই করা হয়েছিল একটি ছোট script দিয়ে। সত্যিকারের run, Python 3.14.4, NumPy 2.5.4:

~/bazarghar-ai$ python numpy2_check.py
NumPy 2.5.4
np.float64(1.5) | str: 1.5 | float: 1.5
np.float: module 'numpy' has no attribute 'float'.
np.float_: `np.float_` was removed in the NumPy 2.0 release. Use `np.float64` instead.
np.NaN: `np.NaN` was removed in the NumPy 2.0 release. Use `np.nan` instead.
np.bool: <class 'numpy.bool'> | default int: int64
str array dtype: <U6
StringDType: StringDType()
int8 + 200: OverflowError: Python integer 200 out of bounds for int8

Error বার্তাগুলো নিজেই সংশোধনের পথ বলে দেয় — এটি NumPy দলের একটি সচেতন নকশা। Scalar-এর নতুন ছাপা রূপ নিয়ে একটি ব্যবহারিক কথা: কোনো list-এর ভেতরে NumPy সংখ্যা থাকলে print([total]) দেখাবে [np.int64(1624)]. ভুল নয়, শুধু দেখতে ভিন্ন; রিপোর্ট বা JSON-এ পাঠানোর আগে int() বা float() দিয়ে Python সংখ্যায় নিন, অথবা f-string-এ ছাপুন। পুরোনো codebase NumPy 2-এ নিতে NumPy-র migration guide দেখুন; Ruff linter-এর NPY201 নিয়ম এমন অনেক পুরোনো নাম নিজে খুঁজে দেয় (Ruff-এর পরিচয় পর্ব ১১-এ)।

আরও গভীরে। ndarray-এর মেমরি-নকশা অন্য library-র সঙ্গে ভাগ করার জন্য Python-এ “buffer protocol” (PEP 3118) আর NumPy-র __array_interface__ আছে; এ কারণেই pandas, scikit-learn, Pillow বা PyTorch কপি ছাড়াই অনেক সময় একই মেমরি পড়তে পারে। ২০২০ সালের পর থেকে Python Data API Standards Consortium একটি সাধারণ “array API standard” লিখছে, যাতে একই কোড NumPy, CuPy বা PyTorch-এর array-তে চলে; NumPy 2-এর কয়েকটি নাম-বদল এরই অংশ। Promotion-এর নিয়ম NEP 50-এ আর scalar-এর ছাপা রূপ NEP 51-এ বিস্তারিত।

পরীক্ষা ও ইন্টারভিউতে (বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এ ধরনের প্রশ্ন সাধারণ):

  • Python list আর NumPy array-র মূল পার্থক্য? List-এ যেকোনো type-এর object-এর ঠিকানা; array-তে এক dtype-এর কাঁচা সংখ্যা টানা মেমরিতে, আকার বাঁধা, হিসাব compiled loop-এ।
  • Shape (3, 5)-এর float32 array-র nbytes? ৩ × ৫ = ১৫টি ঘর × ৪ বাইট = ৬০ বাইট।
  • Strides কী, আর transpose কেন বিনা খরচে? প্রতি মাত্রায় এক ধাপ সরতে কত বাইট; transpose শুধু shape আর strides উল্টায়, buffer কপি করে না।
  • ravel বনাম flatten? ravel পারলে view দেয়, flatten সবসময় কপি।
  • np.array([100, 120], dtype=np.int8) * 2-এর ফল? [-56 -16] — ২০০ − ২৫৬ আর ২৪০ − ২৫৬; warning ছাড়াই overflow.

১০ · AI-এর যুগে — AI সহকারীর NumPy কোড যাচাই

NumPy কোড লেখায় AI সহকারী খুব কাজের, কারণ এর API বিশাল আর নামগুলো মনে রাখা কঠিন। কিন্তু সহকারীর শেখা ডেটার বড় অংশ NumPy 1-এর যুগের, আর dtype আর shape-এর ভুল নীরব। AI সহকারী প্রায়ই যে ভুলগুলো করে:

AI সহকারীর ভুলকীভাবে ধরবেন
পুরোনো নাম: np.float, np.int, np.float_, np.NaNচালানো; NumPy 2-এর release notes; Ruff-এর NPY201
“মেমরি বাঁচাতে” int8/int16 বা float16, সীমা না দেখেnp.iinfo/np.finfo দিয়ে সীমা; বড় মানের নমুনা দিয়ে চালানো
dtype না লেখা, এক যন্ত্রে int64 আর অন্য যন্ত্রে int32 ধরে নেওয়াযেখানে দরকার dtype= স্পষ্ট; arr.dtype ছাপা
Transpose-এর জায়গায় reshape, বা উল্টোটাShape-এর সঙ্গে কয়েকটি ঘরের মান হাতে মিলিয়ে দেখা
অস্তিত্বহীন function বা parameterনিজের version-এর numpy.org docs-এ নাম খোঁজা
Loop-এ np.append, বা অকারণে Python loopছোট ডেটায় সময় মাপা; vectorised রূপ খোঁজা
“NumPy ১০০ গুণ দ্রুত” জাতীয় সাধারণ দাবিনিজের ডেটা আর যন্ত্রে মাপা, কয়েকবার

AI সহকারীকে এমনভাবে বলুন, যাতে সে dtype আর shape-এর অনুমানগুলো নিজেই সামনে আনে। লক্ষ করুন — আসল বিক্রির ডেটা নয়, শুধু গঠন আর মানের সীমা দেওয়া হয়েছে:

আমি Python 3.14 আর NumPy 2.5 ব্যবহার করি। বাজারঘরের দৈনিক বিক্রির একটি array আছে: shape (জেলা, দিন), মান হাজার টাকায় ০ থেকে ৫০০, আর দৈনিক অর্ডার-সংখ্যার আরেকটি array, মান ০ থেকে ২০,০০০। ১. দুটির জন্য কোন dtype নেবে, প্রতিটির কারণ আর np.iinfo দিয়ে সীমা-যাচাই দেখাও; ঈদের সময় মান তিন গুণ হতে পারে ধরে নাও। ২. (জেলা, দিন) থেকে (দিন, জেলা) করার কোড দাও, আর বলো reshape না transpose, কেন। ৩. প্রতিটি ধাপের পরে shape আর dtype যাচাইয়ের assert দাও। ৪. NumPy 2-এ সরানো কোনো নাম (np.float, np.float_, np.NaN) ব্যবহার করবে না। ৫. কোনো function বা parameter নিয়ে নিশ্চিত না হলে "নিশ্চিত নই" লেখো আর numpy.org-এর কোন পাতায় যাচাই করব বলো।

AI সহকারীর উত্তর হাতে পেলে — যাচাইয়ের চেকলিস্ট

Array মানে সংখ্যা নয় শুধু — সংখ্যা, তাদের dtype আর তাদের shape. তিনটিই যাচাই না করলে হিসাব যাচাই হয়নি।

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

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

প্র ০১Numeric আর Numarray-র বিভাজন কীভাবে NumPy-তে মিলল, আর একটি ecosystem কেন একটি সাধারণ array type-এর ওপর দাঁড়ায়?

১৯৯০-এর দশকের মাঝামাঝি Python-এ সংখ্যার array-র জন্য Numeric package তৈরি হয়; Jim Hugunin-কে এর প্রধান প্রাথমিক লেখক হিসেবে উল্লেখ করা হয়, আর Python-এর matrix নিয়ে আগ্রহী একটি দল এর নকশায় অংশ নেয়। ২০০০-এর দশকের শুরুতে Space Telescope Science Institute-এর দল বড় ছবি আর ডেটার জন্য Numeric-এর কিছু সীমাবদ্ধতা দূর করতে Numarray বানায়। দুটির ভালো-মন্দ আলাদা ছিল: ছোট array-তে Numeric দ্রুত বলে পরিচিত ছিল, বড় array আর নমনীয় dtype-এ Numarray. ফলে library-লেখকদের দুটির জন্যই কোড রাখতে হতো, বা একটি বেছে নিতে হতো — সম্প্রদায় কার্যত দুই ভাগে বিভক্ত হয়।

২০০৫–২০০৬ সালে Travis Oliphant Numarray-র কয়েকটি ধারণা Numeric-এর ভিত্তিতে মিলিয়ে NumPy তৈরি করেন, আর NumPy 1.0 প্রকাশিত হয় ২০০৬ সালে। এর পর ধীরে ধীরে SciPy, matplotlib, পরে pandas আর scikit-learn সবাই একই ndarray-কে সাধারণ ভাষা হিসেবে নেয়। ২০২০ সালের Nature প্রবন্ধে (Harris ও সহকর্মীরা) এই ভূমিকাকে বৈজ্ঞানিক Python-এর ভিত্তি হিসেবে বর্ণনা করা হয়েছে।

কেন একটি সাধারণ type এত গুরুত্বপূর্ণ? ভাবুন যদি প্রতিটি মোবাইল ব্যাংকিং সেবার আলাদা “টাকা” থাকত, আর এক সেবা থেকে আরেক সেবায় পাঠাতে প্রতিবার রূপান্তর লাগত। ডেটার কাজে প্রতিটি library যদি নিজের array বানাত, তাহলে pandas থেকে scikit-learn-এ ডেটা দিতে প্রতিবার কপি আর রূপান্তর লাগত — ধীর, মেমরি-ক্ষুধার্ত আর ভুলপ্রবণ। একটি সাধারণ মেমরি-নকশা (buffer, dtype, shape, strides) থাকায় library-গুলো একে অপরের ডেটা প্রায়ই কপি ছাড়া পড়তে পারে।

আজ আবার একটি নতুন রূপে একই প্রশ্ন উঠেছে: GPU-র জন্য CuPy, deep learning-এর PyTorch আর JAX-এর নিজস্ব array আছে। তাই “array API standard” আর DLPack-এর মতো উদ্যোগ চেষ্টা করছে যাতে একই কোড অনেক array library-তে চলে আর ডেটা কপি ছাড়াই হাতবদল হয়। বিভাজন থেকে একীকরণ, আবার নতুন বিভাজন আর নতুন সেতু — ইতিহাসটি এখনো চলছে।

মূল উপলব্ধি: NumPy-র সবচেয়ে বড় অবদান হয়তো গতি নয়, একটি সাধারণ ভাষা। একটি ecosystem-এ সবাই একই ডেটা-কাঠামো বুঝলে প্রতিটি নতুন library বাকি সবার কাজ সঙ্গে সঙ্গে ব্যবহার করতে পারে।

প্র ০২বাস্তব জগতে integer overflow কী ক্ষতি করেছে, আর NumPy-তে dtype বাছাইয়ের সময় সেখান থেকে কী শেখার আছে?

সবচেয়ে বিখ্যাত উদাহরণগুলোর একটি ১৯৯৬ সালের Ariane 5 রকেটের প্রথম উড্ডয়ন। তদন্ত প্রতিবেদন অনুযায়ী উৎক্ষেপণের প্রায় ৪০ সেকেন্ড পরে রকেটটি পথ হারিয়ে ধ্বংস হয়। কারণ হিসেবে চিহ্নিত হয় দিক-নির্ণয় ব্যবস্থার একটি software: একটি ৬৪-বিট দশমিক সংখ্যাকে ১৬-বিটের চিহ্নসহ পূর্ণসংখ্যায় রূপান্তর করতে গিয়ে মানটি সীমার বাইরে চলে যায়। কোডটি আগের Ariane 4 থেকে নেওয়া হয়েছিল, যেখানে ওই মান কখনো এত বড় হতো না; Ariane 5-এর ভিন্ন গতিপথে সেই অনুমান ভেঙে যায়। প্রতিবেদন শুধু রূপান্তরকে নয়, পুরোনো অনুমান নতুন পরিবেশে যাচাই না করাকেও বড় কারণ বলেছে।

একটি নিরীহ উদাহরণ ২০১৪ সালের: একটি জনপ্রিয় গানের ভিডিওর দেখার সংখ্যা ৩২-বিটের চিহ্নসহ পূর্ণসংখ্যার সীমা ২,১৪৭,৪৮৩,৬৪৭-এর কাছে পৌঁছালে ভিডিও প্ল্যাটফর্মটি গণনাটি ৬৪-বিটে নেওয়ার কথা জানিয়েছিল বলে ব্যাপকভাবে প্রচারিত। এ ছাড়া বিমান চলাচল-নিয়ন্ত্রক সংস্থার নির্দেশনায় একটি যাত্রীবাহী বিমানের ক্ষেত্রে টানা প্রায় ২৪৮ দিন বিদ্যুৎ চালু থাকলে একটি গণনা-counter-এর overflow-এর ঝুঁকির কথা এসেছিল; সমাধান হিসেবে নির্দিষ্ট সময় পরপর ব্যবস্থা নতুন করে চালু করার নির্দেশ দেওয়া হয়।

তিনটি ঘটনার মিল: প্রতিটিতে কেউ একসময় ধরে নিয়েছিলেন “মান এত বড় হবে না”। NumPy-তে এই অনুমানই আপনি নেন যখন int8, int16 বা int32 বেছে নেন। আর ঝুঁকিটা বেশি, কারণ array-র overflow কোনো warning দেয় না। বাজারঘরের ছোট অর্ডার-সংখ্যা আজ ১১৩, ঈদে ২২৬ — আর int8-এ তা −৩০।

ব্যবহারিক শিক্ষা: dtype বাছাই করুন সবচেয়ে বড় সম্ভাব্য মান ধরে, আজকের গড় ধরে নয়। “সবচেয়ে বড় মান” ভাবার সময় যোগফল আর গুণফলও ধরুন, কারণ ডেটা নিজে ছোট হলেও তার ওপর হিসাব বড় হতে পারে। অনুমানগুলো কোডে লিখে রাখুন (assert values.max() <= np.iinfo(np.int16).max), যাতে পরিবেশ বদলালে — যেমন Ariane 4 থেকে 5 — অনুমানটি নিজে চিৎকার করে।

মূল উপলব্ধি: dtype একটি চুক্তি: “আমার মান এই সীমার ভেতরে থাকবে।” চুক্তিটি লিখে রাখুন আর যাচাই করুন; নইলে কোড পুনর্ব্যবহারের দিনে চুক্তিটি নীরবে ভাঙবে।

প্র ০৩Deep learning কেন float64 ছেড়ে float32, এমনকি float16 আর bfloat16-এ গেল, আর এর দাম কী?

বৈজ্ঞানিক হিসাবে ঐতিহ্যগতভাবে float64 (double precision) স্বাভাবিক পছন্দ, আর NumPy-র default float-ও তাই। কিন্তু neural network-এর প্রশিক্ষণে কোটি কোটি parameter আর বিশাল ডেটা বারবার গুণ-যোগ হয়। সেখানে দুটি জিনিস প্রধান হয়ে ওঠে: মেমরি আর গতি। float32 অর্ধেক মেমরি নেয়, একই মেমরি-পথে দ্বিগুণ সংখ্যা আনে, আর GPU-গুলো সাধারণত float32-এ float64-এর চেয়ে অনেক দ্রুত। আর প্রশিক্ষণের হিসাব এমনিতেই gradient-এর আনুমানিক ধাপে এগোয়, তাই float64-এর বাড়তি নির্ভুলতা প্রায়ই কাজে লাগে না।

পরের ধাপে আসে ১৬ বিট। ২০১৮ সালে Micikevicius ও সহকর্মীদের প্রবন্ধ “Mixed Precision Training” (ICLR) দেখায় যে বেশিরভাগ হিসাব float16-এ করে আর ওজনের একটি float32 “মূল কপি” রেখে, সঙ্গে loss scaling নামের কৌশলে, অনেক model-এ প্রায় একই মান পাওয়া যায়। float16-এর সমস্যা তার ছোট পরিসর: খুব ছোট gradient শূন্য হয়ে যায়, যাকে বলে underflow. Google-এর গবেষকদের প্রচলিত bfloat16 অন্য পথ নেয়: float32-এর সমান exponent (তাই একই পরিসর), কিন্তু অনেক কম নির্ভুলতা। অনেক আধুনিক accelerator এখন এটি সমর্থন করে, আর বড় ভাষা-model চালাতে আরও ছোট ৮-বিট বা ৪-বিট রূপও (quantization) ব্যবহার হয়।

দাম কী? কম নির্ভুলতায় যোগফলে ছোট সংখ্যা হারিয়ে যায়, প্রশিক্ষণ মাঝে মাঝে অস্থির হয়, আর ফল এক hardware থেকে আরেকটিতে সামান্য বদলাতে পারে। তাই loss-এর মতো সংবেদনশীল যোগফল প্রায়ই বেশি নির্ভুলতায় রাখা হয়। পরিসংখ্যান, অর্থের হিসাব বা ছোট ডেটার classical machine learning-এ এই লেনদেন সাধারণত লাভজনক নয় — সেখানে float64-ই নিরাপদ।

এই পর্বের সঙ্গে যোগসূত্র: ওপরের লেখকের run-এ float32-এর হিসাব float64-এর প্রায় অর্ধেক সময় নিয়েছে, শুধু অর্ধেক বাইট আনতে হয়েছে বলে। Deep learning-এর পূর্ণ আলোচনা এই কোর্সের পরে, deep learning কোর্সে আসবে।

মূল উপলব্ধি: নির্ভুলতা বিনামূল্যে নয়, আবার সবসময় দরকারিও নয়। কাজের প্রকৃতি — পরিসংখ্যান না প্রশিক্ষণ, টাকা না pixel — ঠিক করে কতটা নির্ভুলতা কেনা উচিত।

প্র ০৪Row-major (C) আর column-major (Fortran) — দুই রীতির উৎপত্তি কোথায়, আর NumPy ব্যবহারকারীর কাছে এটি কখন গুরুত্বপূর্ণ?

দুই মাত্রার ছককে এক লাইনের মেমরিতে রাখার দুই স্বাভাবিক উপায়: সারি ধরে (row-major), অথবা কলাম ধরে (column-major) রাখা। C ভাষা ও তার উত্তরসূরিরা সারি ধরে রাখে; Fortran — বৈজ্ঞানিক হিসাবের পুরোনো ভাষা, ১৯৫০-এর দশকে IBM-এ John Backus-এর দলের তৈরি — কলাম ধরে। MATLAB, R আর Julia-ও column-major. NumPy-র default C order, কিন্তু order="F" দিয়ে Fortran order-এও array রাখা যায়, আর একটি transpose করা C array আসলে Fortran order-এর মতোই পড়া হয়।

কেন গুরুত্বপূর্ণ? প্রথমত, গতি। অংশ ৮-এ দেখেছেন, CPU মেমরি খণ্ডে খণ্ডে আনে। C order-এর array-তে শেষ মাত্রা ধরে (একই সারির পাশাপাশি ঘর) হাঁটলে প্রতিটি খণ্ড পুরো কাজে লাগে; প্রথম মাত্রা ধরে লাফালে (৫৬ বাইট, বা বড় ছকে কয়েক কিলোবাইট করে) খণ্ডের বেশিরভাগ নষ্ট হয়। তাই বিশাল array-তে “সারি ধরে যোগ” আর “কলাম ধরে যোগ” একই হিসাব হলেও সময় আলাদা হতে পারে।

দ্বিতীয়ত, অন্য library-র সঙ্গে হাতবদল। Linear algebra-র ভিত্তি BLAS আর LAPACK-এর ঐতিহ্য Fortran-এর, তাই সেগুলো কলাম ধরে ডেটা আশা করতে পারে; NumPy আর SciPy এই রূপান্তর সাধারণত নিজেরাই সামলায়, কখনো একটি অদৃশ্য কপির খরচে। C-তে লেখা কোনো extension বা ফাইলে কাঁচা বাইট লিখে অন্য ভাষায় পড়ার সময় order ভুল হলে ছক উল্টে যায় — আর কোনো error আসে না, কারণ বাইটের সংখ্যা একই।

তৃতীয়ত, reshape-এর অর্থ। reshape-এর নিজেরও একটি order argument আছে; C আর F order-এ একই reshape ভিন্ন ছক দেয়। MATLAB বা R থেকে কোড অনুবাদের সময় এটি একটি পরিচিত ফাঁদ।

মূল উপলব্ধি: মেমরি এক-মাত্রার; ছক একটি ব্যাখ্যা মাত্র। কোন ব্যাখ্যা (C না F) চলছে, তা জানলে গতির রহস্য আর ভাষা-বদলের নীরব বাগ — দুটোই বোঝা যায়।

প্র ০৫Python নিজে ধীর, তবু AI-এর প্রধান ভাষা কেন Python — “দুই ভাষার সমস্যা” কী, আর এর বিকল্প নিয়ে বিতর্ক কোথায়?

এই পর্বের মাপেই দেখেছেন, সংখ্যার ওপর Python loop ধীর। তবু NumPy, pandas, scikit-learn, PyTorch — AI-এর প্রায় সব প্রধান টুলের মুখ Python. রহস্যটি NumPy-র নকশাতেই: Python কাজ করে “আঠা” হিসেবে, অর্থাৎ কী হিসাব হবে তা বলে দেয়, আর আসল ভারী কাজ চলে C, C++, Fortran বা GPU-র CUDA-য় লেখা কোডে। বিশাল একটি matrix গুণে Python-এর খরচ কয়েক মাইক্রোসেকেন্ড, আর আসল গুণ চলে অপ্টিমাইজড library-তে। মানুষের সময় (লেখা, পড়া, পরীক্ষা) বাঁচে Python-এ, যন্ত্রের সময় বাঁচে compiled কোডে।

এর একটি দামও আছে, যাকে প্রায়ই “দুই ভাষার সমস্যা” (two-language problem) বলা হয়: গবেষক Python-এ ধারণা পরীক্ষা করেন, কিন্তু নতুন কোনো দ্রুত কাজ লাগলে সেটি C বা C++-এ লিখতে হয়। আর যে হিসাব vectorise করা যায় না — প্রতিটি ধাপ আগের ফলের ওপর নির্ভর করে, অনেক শর্তসহ — সেখানে Python আবার ধীর। ২০১২ সালে শুরু হওয়া Julia ভাষা ঠিক এই সমস্যা মেটানোর লক্ষ্য ঘোষণা করে এসেছিল: লেখা Python-এর মতো সহজ, গতি C-এর কাছাকাছি।

Python জগৎও বসে নেই। Numba আর Cython Python-ধাঁচের কোডকে compiled কোডে নেয়; JAX আর PyTorch-এর compiler পুরো হিসাবের নকশা ধরে অপ্টিমাইজ করে; CPython নিজেও ৩.১১ থেকে উল্লেখযোগ্য গতি পেয়েছে, আর ৩.১৩ থেকে GIL ছাড়া চলার একটি পরীক্ষামূলক রূপ (PEP 703) এসেছে। কোন পথ জিতবে, তা নিয়ে সম্প্রদায়ে মতভেদ আছে; বাস্তবে Python-এর বিশাল library-ভাণ্ডার আর শেখার সহজতা এখনো বড় সুবিধা, আর নতুন ভাষাগুলো নির্দিষ্ট জায়গায় জায়গা করে নিচ্ছে।

আপনার জন্য ব্যবহারিক অর্থ: Python-এ AI কাজ দ্রুত করার প্রথম কৌশল নতুন ভাষা নয়, বরং ভারী loop-গুলো NumPy-র মতো library-র ভেতরে ঠেলে দেওয়া — যা এই মডিউলের মূল দক্ষতা।

মূল উপলব্ধি: Python দ্রুত নয়, Python দ্রুত জিনিস ডাকতে দক্ষ। সেই কারণেই array-র মেমরি-নকশা বোঝা AI-এর Python-এ গতির চাবিকাঠি।

অনুশীলন

হিসাব, আগাম অনুমান, কোড আর নকশা-সমালোচনা — চার রকম কাজ।

  1. হিসাব করুন: বাজারঘরের ৬৪ জেলা × ৩৬৫ দিনের বিক্রি একটি C-order int64 array-তে। (ক) size, nbytes আর strides কত? (খ) ঘর [10, 100] buffer-এর শুরু থেকে কত বাইট দূরে? (গ) একই ডেটা float32-এ নিলে nbytes আর strides কত?

    (ক) size = ৬৪ × ৩৬৫ = ২৩,৩৬০। nbytes = ২৩,৩৬০ × ৮ = ১,৮৬,৮৮০ বাইট। পরের দিনে যেতে ৮ বাইট, পরের জেলায় যেতে এক সারি = ৩৬৫ × ৮ = ২,৯২০ বাইট; তাই strides (2920, 8).

    (খ) offset = ১০ × ২,৯২০ + ১০০ × ৮ = ২৯,২০০ + ৮০০ = ৩০,০০০ বাইট — অর্থাৎ ৩,৭৫০ নম্বর ঘর (৩০,০০০ ÷ ৮)। মিলিয়ে নিন: ১০ × ৩৬৫ + ১০০ = ৩,৭৫০।

    (গ) float32-এ itemsize ৪, তাই nbytes = ৯৩,৪৪০ বাইট আর strides (1460, 4). ঘরের সংখ্যা একই, শুধু প্রতিটি ঘর অর্ধেক।

  2. আগাম বলুন (predict the output): চালানোর আগে লিখে ফেলুন, নিচের তিনটি print কী দেখাবে:
    a = np.arange(1, 7).reshape(2, 3)
    print(a.T)
    print(a.reshape(3, 2))
    print(np.array([1, 2, 3], dtype=np.int8) * 50)

    a হলো [[1 2 3], [4 5 6]]. Transpose সারি-কলাম অদলবদল করে: [[1 4] [2 5] [3 6]]. reshape(3, 2) buffer-এর ক্রম 1 2 3 4 5 6 দুটি দুটি করে কাটে: [[1 2] [3 4] [5 6]]. দুটোই shape (3, 2), কিন্তু ভিন্ন।

    তৃতীয়টি: ৫০, ১০০, ১৫০ — কিন্তু ১৫০ int8-এর সীমা ১২৭ পেরিয়েছে, তাই ১৫০ − ২৫৬ = −১০৬। ফল [ 50 100 -106], কোনো warning ছাড়া। Python 3.14.4 আর NumPy 2.5.4-এ চালিয়ে মেলানো।

  3. কোড লিখুন: একটি function smallest_int_array(values) লিখুন, যা int8, int16, int32, int64-এর মধ্যে সবচেয়ে ছোট সেই dtype বেছে array বানায়, যাতে সব মান ধরে; কোনোটিতে না ধরলে OverflowError. পরীক্ষা করুন [92, 113], [92, 300], [-40000, 5] আর [3_000_000_000] দিয়ে। তারপর ভাবুন: এই function কি বাজারঘরের অর্ডার-সংখ্যার জন্য নিরাপদ?
    import numpy as np
    
    def smallest_int_array(values):
        lo, hi = min(values), max(values)
        for dt in (np.int8, np.int16, np.int32, np.int64):
            info = np.iinfo(dt)
            if info.min <= lo and hi <= info.max:
                return np.array(values, dtype=dt)
        raise OverflowError("values do not fit in int64")
    
    for v in ([92, 113], [92, 300], [-40000, 5], [3_000_000_000]):
        a = smallest_int_array(v)
        print(v, a.dtype, a.nbytes)

    নিজের যন্ত্রে চালানো ফল: [92, 113] int8 2, [92, 300] int16 4, [-40000, 5] int32 8, [3000000000] int64 8. ৩০০ int8-এ ধরে না, −৪০,০০০ int16-এ ধরে না (সীমা −৩২,৭৬৮), আর ৩০০ কোটি int32-এর সীমা প্রায় ২১৪ কোটি পেরোয়।

    নিরাপদ কি? সংরক্ষণের জন্য হয়তো, হিসাবের জন্য নয়। আজকের সর্বোচ্চ ১১৩ দেখে int8 বেছে নিলে ঈদের দ্বিগুণে overflow হবে — এই পর্বের খুলনার উদাহরণ ঠিক এটাই। তাই এমন function দিয়ে ছোট করে রাখলে হিসাবের আগে astype(np.int64) করুন, অথবা সর্বোচ্চ সম্ভাব্য মানের সঙ্গে একটি নিরাপত্তা-ব্যবধান রেখে dtype বেছে নিন।

  4. নকশা-সমালোচনা: একজন সহকর্মী AI সহকারী দিয়ে একটি script বানিয়েছেন: data = np.array(rows) (rows-এ জেলার নাম আর বিক্রি একসঙ্গে); sales = data[:, 1:].astype(np.float16) “মেমরি বাঁচাতে”; daily = sales.reshape(7, 4) দিয়ে (৪ জেলা × ৭ দিন) থেকে (দিন × জেলা); total = sales.sum(dtype=np.float); আর গতির জন্য loop-এ result = np.append(result, x). অন্তত পাঁচটি সমস্যা আর প্রতিটির সংশোধন বলুন।
    • নাম আর সংখ্যা এক array-তে: একটি লেখা থাকলেই পুরো array <U লেখা হয়ে যায়; সংখ্যা আলাদা array-তে dtype=np.int64 দিয়ে, নাম আলাদা list-এ (বা pandas-এ, মডিউল ৪)।
    • float16 টাকায়: প্রায় ৩টি অর্থবহ অঙ্ক আর সর্বোচ্চ ৬৫,৫০৪; যোগফল আর বড় টাকায় ভুল হবে। হাজার টাকার পূর্ণসংখ্যায় int64, বা দরকারে float64.
    • reshape দিয়ে “transpose”: reshape(7, 4) সংখ্যাগুলো buffer-এর ক্রমে আবার কাটে, ফলে দিন আর জেলা মিশে যায় — নীরব বাগ। দরকার sales.T.
    • np.float: NumPy 1.24 থেকে নেই, AttributeError দেবে; np.float64 বা Python float.
    • Loop-এ np.append: প্রতিবার পুরো array কপি — বড় ডেটায় খুব ধীর। list-এ জমিয়ে শেষে np.array, অথবা পুরো কাজটি vectorised রূপে।
    • যাচাই নেই: প্রতিটি ধাপে shape আর dtype-এর assert, আর কয়েকটি ঘরের মান হাতে মেলানো।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমShape (4, 7), strides (56, 8)-এর একটি int64 array থেকে sales[2, 3] পড়তে NumPy যে ধাপগুলো নেয়, সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📐 Shape দেখে যাচাই: ২ < ৪ আর ৩ < ৭, index সীমার ভেতরে
  2. 🧮 Strides দিয়ে offset হিসাব: ২ × ৫৬ + ৩ × ৮ = ১৩৬ বাইট
  3. 📍 Buffer-এর শুরুর ঠিকানার সঙ্গে ১৩৬ বাইট যোগ
  4. 🔢 dtype অনুযায়ী সেখান থেকে ৮ বাইট পড়ে int64 হিসেবে ব্যাখ্যা
  5. 📦 মানটিকে একটি NumPy scalar (np.int64) হিসেবে ফেরত দেওয়া

Shape (3, 5)-এর একটি float32 array-র nbytes কত?

  • ১৫
  • ৬০
  • ১২০
  • ৩২

np.array([1, 2.5, True])-এর dtype কী হবে?

  • int64
  • object
  • একটি float64 array, যেখানে True হয়ে যায় 1.0
  • <U32

a = np.array([100, 120], dtype=np.int8). a * 2 কী দেয়?

  • [-56 -16], কোনো warning ছাড়াই
  • [200 240], dtype int16-এ উঠে
  • OverflowError
  • [127 127]

Shape (4, 7), strides (56, 8)-এর একটি int64 array s. s.T সম্পর্কে কোনটি সঠিক?

  • ২২৪ বাইটের নতুন একটি কপি তৈরি হয়, shape (7, 4)
  • Shape (7, 4), strides (56, 8) অপরিবর্তিত
  • সংখ্যাগুলো buffer-এর ক্রমে সাতটি সারিতে চারটি করে কাটা হয়
  • Shape (7, 4), strides (8, 56), একই buffer

NumPy 2.x-এ x = np.zeros(3, dtype=np.float) চালালে কী হবে?

  • float64-এর তিনটি শূন্য
  • AttributeError; লিখতে হবে float বা np.float64
  • float32-এর তিনটি শূন্য
  • একটি DeprecationWarning, তারপর float64

একই ১০ লাখ সংখ্যার যোগফলে NumPy array সাধারণত Python list-এর loop-এর চেয়ে অনেক দ্রুত। এর সবচেয়ে সঠিক ব্যাখ্যা কোনটি?

  • NumPy সব হিসাব GPU-তে পাঠায়
  • NumPy নির্ভুলতা কমিয়ে আনুমানিক যোগফল দেয়
  • টানা typed buffer-এর ওপর compiled loop, প্রতি ঘরে Python-এর খরচ ছাড়া
  • NumPy কিছু সংখ্যা বাদ দিয়ে নমুনা নিয়ে যোগ করে

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. ndarray = টানা typed buffer + metadata (dtype, shape, strides); list হলো ছড়ানো boxed object-এর ঠিকানার সারি।
  2. nbytes = size × itemsize, আর যেকোনো ঘরের offset = i × s0 + j × s1.
  3. dtype একটি চুক্তি: মিশ্র type-এ upcasting হয়, কিন্তু ছোট int-এর array-হিসাব নীরবে overflow করে; সন্দেহ থাকলে int64, আর dtype স্পষ্ট লিখুন — ব্রাউজারের Python-এ default int হলো int32.
  4. reshape আর transpose আলাদা; দুটোই সাধারণত শুধু header বদলায়। ravel পারলে view, flatten সবসময় কপি।
  5. গতি আসে টানা মেমরি, প্রতি ঘরে Python-এর খরচ না থাকা আর compiled loop থেকে; নিজের ডেটায় মেপে দেখুন। NumPy 2-এ পুরোনো alias নেই, scalar ছাপা হয় np.float64(1.5).

পরের পর্বে: বিক্রির ছক থেকে ঠিক যা দরকার তা বেছে নেওয়া — একটি জেলা, একটি সপ্তাহের অংশ, ৫০ হাজারের বেশি বিক্রির দিনগুলো। Boolean mask, fancy index, আর কখন বেছে নেওয়া অংশ একটি view, কখন নতুন কপি।

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

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান