سرنخ — خودآموز برنامه‌نویسی برای هوش مصنوعی (مقدماتی)

فصل ۱ از ۱۰

پیشرفت ترم
۰٪

ترم ۲ · داده: دیدن، تمیز کردن، فهمیدن

چرا آرایه: اولین قدمِ کارِ داده

فصل ۱پیش‌نمایش رایگان
۹ دقیقه مطالعه فصل ۱

در این فصل چه یاد می‌گیری#

در ترمِ یک با فهرست کار کردی و برای هر کاری حلقه نوشتی. برای چهار محصول عالی بود. برای دویست‌هزار ردیف نه — نه فقط چون کند است، بلکه چون کدش خوانا نیست و اشتباهش دیر پیدا می‌شود. در این فصل numpy را می‌بینی و اولین ساختارِ دادهٔ واقعیِ دنیای علمِ داده را: آرایه. یاد می‌گیری چرا آرایه از فهرست فرق دارد، shape چیست و چرا مهم‌ترین چیزی است که باید همیشه بدانی، و چطور یک جدول در حافظه شکل می‌گیرد.

یک قفسهٔ برش‌خوردهٔ چندطبقه که همهٔ خانه‌هایش دقیقاً هم‌اندازه‌اند، کنارِ کیسه‌ای از اشیاء نامنظم

آخر این فصل می‌توانی:

  • یک ndarray بسازی و shape و dtype آن را بخوانی
  • بگویی آرایه از list در چه چیزی فرق دارد و چرا این تفاوت اهمیت دارد
  • آرایهٔ آماده بسازی با zeros، arange و linspace
  • یک آرایهٔ دوبعدی بسازی و بگویی هر عددِ shape یعنی چه
  • خطای ناسازگاریِ شکل را تشخیص بدهی

قبل از شروع#

از ترمِ ۱ فصل ۳: list، اندیس از صفر، و برش.

از ترمِ ۱ فصل ۶: حلقه و الگوی انباشتگر — که در این ترم کم‌کم جایش را به چیزِ بهتری می‌دهد.

📓 نوت‌بوک: نوت‌بوک این فصل را در Colab باز کن — همهٔ کدهای این فصل آماده و به‌ترتیب داخلش هست.

۱. اولین آرایه#

numpy در Colab از قبل نصب است. فقط import می‌خواهد، و قرارداد جهانی این است که با نامِ np صدایش بزنی.

import numpy as np

prices = np.array([850000, 1200000, 430000, 2100000])

print(prices)
print(type(prices))
print(prices.shape)
print(prices.dtype)
[ 850000 1200000  430000 2100000]
<class 'numpy.ndarray'>
(4,)
int64

سه چیزِ تازه اینجاست:

  • ndarray نامِ نوعِ آرایه در numpy است (کوتاه‌شدهٔ «آرایهٔ n-بُعدی»).
  • shape شکلِ آرایه است، به‌صورتِ tuple. اینجا (4,) یعنی «یک بُعد، با چهار عنصر». آن ویرگولِ تنها یادت می‌اندازد که tuple است، نه عدد.
  • dtype نوعِ عنصرها را می‌گوید — و اینجا اولین تفاوتِ بزرگ با list است: همهٔ عنصرهای یک آرایه یک نوع دارند. فهرست می‌توانست عدد و متن را قاطی نگه دارد؛ آرایه نمی‌تواند.

این محدودیت، در واقع همان چیزی است که آرایه را سریع می‌کند: وقتی همه یک نوع‌اند، numpy می‌داند هر عنصر دقیقاً چند بایت است و می‌تواند کلِ بلوکِ حافظه را یکجا پردازش کند.

چک کن: shape باید (4,) باشد و dtype باید int64. اگر dtype را float64 دیدی، یعنی یکی از اعدادت اعشاری بوده — numpy همه را به بالاترین نوعِ مشترک می‌برد.

۲. وقتی نوع‌ها قاطی می‌شوند#

mixed = np.array([1, 2.5, 3])
text_like = np.array([1, "دو", 3])

print(mixed, mixed.dtype)
print(text_like, text_like.dtype)
[1.  2.5 3. ] float64
['1' 'دو' '3'] <U21

آرایهٔ اول همه را float کرد، چون عددِ صحیح در اعشاری جا می‌شود ولی برعکسش نه. آرایهٔ دوم — که یک متن داشت — همه‌چیز را متن کرد، حتی عددها. آن <U21 یعنی «رشتهٔ یونیکد»؛ عددِ کنارش فقط بیشترین طولی است که numpy برای این ستون کنار گذاشته، نه چیزِ مهمی.

این دومی یک تلهٔ واقعی است: هیچ خطایی نگرفتی، ولی حالا آرایه‌ات از عدد به متن تبدیل شده و هیچ محاسبه‌ای رویش کار نمی‌کند. در همین ترم، فصلِ ۵، دقیقاً همین اتفاق روی یک ستونِ قیمتِ واقعی می‌افتد — و آنجا می‌بینی چقدر بی‌سروصدا اتفاق می‌افتد.

۳. آرایه‌های آماده#

اغلب نمی‌خواهی عددها را با دست بنویسی:

print(np.zeros(5))
print(np.ones(3, dtype=int))
print(np.arange(0, 10, 2))
print(np.linspace(0, 1, 5))
[0. 0. 0. 0. 0.]
[1 1 1]
[0 2 4 6 8]
[0.   0.25 0.5  0.75 1.  ]
  • zeros(n) و ones(n) آرایهٔ پُر از صفر یا یک می‌سازند — معمولاً به‌عنوانِ ظرفِ خالی برای پُر کردن.
  • arange(start, stop, step) همان range است ولی خروجی‌اش آرایه است و می‌تواند گامِ اعشاری داشته باشد.
  • linspace(start, stop, n) فرق دارد و خیلی به‌کارت می‌آید: n عددِ هم‌فاصله بینِ دو مقدار می‌دهد و هر دو سر را شامل می‌شود. در arange گام را می‌دهی؛ در linspace تعداد را.

💡 نکته: برای رسمِ نمودار تقریباً همیشه linspace می‌خواهی، نه arange. وقتی می‌گویی «صد نقطه بینِ صفر و یک»، linspace(0, 1, 100) دقیقاً همان است و لازم نیست گام را حساب کنی.

۴. آرایهٔ دوبعدی: شکلِ یک جدول#

دادهٔ واقعی جدول است: ردیف و ستون. در numpy این یک آرایهٔ دوبعدی است.

table = np.array([
    [850000, 12],
    [1200000, 0],
    [430000, 7],
    [2100000, 3],
])

print(table)
print("shape:", table.shape)
print("تعداد بُعد:", table.ndim)
print("تعداد کل عنصر:", table.size)
[[ 850000      12]
 [1200000       0]
 [ 430000       7]
 [2100000       3]]
shape: (4, 2)
تعداد بُعد: 2
تعداد کل عنصر: 8

shape اینجا (4, 2) است: چهار ردیف، دو ستون. ترتیبش همیشه همین است — اول ردیف، بعد ستون — و از این به بعد هر جا shape دیدی باید بتوانی بلافاصله بگویی هر عدد چه چیزی را می‌شمارد.

دسترسی هم دوبخشی می‌شود:

print(table[0])
print(table[0, 1])
print(table[:, 0])
print(table[1:3, :])
[850000     12]
12
[ 850000 1200000  430000 2100000]
[[1200000       0]
 [ 430000       7]]

table[0] کلِ ردیفِ اول را می‌دهد. table[0, 1] یک عنصر: ردیفِ صفر، ستونِ یک. و مهم‌ترینش table[:, 0] است: دونقطهٔ تنها یعنی «همه»، پس این یعنی «همهٔ ردیف‌ها، ستونِ صفر» — یعنی یک ستونِ کامل. این الگو را زیاد می‌بینی.

قبل از ادامه، جواب بده: آرایه‌ای با shape برابرِ (1000, 5) داری. arr[:, 2] چه شکلی دارد و چند عنصر است؟ جواب: shape آن (1000,) است و هزار عنصر دارد — ستونِ سوم برای همهٔ هزار ردیف. برشِ یک ستون، یک بُعد کم می‌کند.

۵. یک خطای واقعی: شکل‌ها نمی‌خوانند#

a = np.array([1, 2, 3])
b = np.array([10, 20, 30, 40])
print(a + b)
ValueError: operands could not be broadcast together with shapes (3,) (4,)

پیام هر دو شکل را دقیقاً می‌گوید: (3,) و (4,). numpy نمی‌تواند حدس بزند کدام عنصر با کدام جمع شود، پس می‌ایستد.

این رایج‌ترین خطای کلِ کارِ داده است و آن کلمهٔ broadcast در پیام، اسمِ قاعده‌ای است که فصلِ بعد کاملاً بازش می‌کند.

🔧 اگر کار نکرد: هر وقت خطایی دیدی که کلمهٔ shape یا broadcast در آن بود، قبل از هر کاری shape هر دو طرف را چاپ کن. در نود درصدِ موارد یکی از آن‌ها همانی نیست که فکر می‌کردی — مثلاً (1000, 1) است به‌جای (1000,). این عادت را از همین‌جا بساز؛ در ترمِ ۳ و در ژرفا بارها نجاتت می‌دهد.

۶. چرا اصلاً این کار را می‌کنیم#

در کدِ زیر سه چیزِ تازه هست که هیچ‌کدام ربطی به numpy ندارند ولی از این به بعد مرتب می‌بینی‌شان:

  • زیرخط داخلِ عدد. 2_000_000 دقیقاً همان 2000000 است؛ پایتون زیرخط‌ها را نادیده می‌گیرد و آن‌ها فقط برای چشمِ تو هستند، مثلِ ویرگولِ هزارگان. برای عددهای بزرگ همیشه این‌طور بنویس.
  • time.perf_counter() یک کرنومتر است: عددی برحسبِ ثانیه می‌دهد که خودش به‌تنهایی معنایی ندارد، ولی تفاضلِ دو بار صدا زدنش می‌گوید بینشان چقدر طول کشیده.
  • {x:.3f} داخلِ f-string. تا حالا داخلِ آکولاد فقط مقدار می‌گذاشتی؛ هر چیزی بعد از دونقطه یک دستورِ قالب‌بندی است. .3f یعنی «عددِ اعشاری با سه رقم بعد از ممیز» و .0f یعنی «بدونِ اعشار». بعداً {x:>8} را هم می‌بینی که یعنی «در هشت خانه، چسبیده به راست» — فقط برای اینکه ستون‌های خروجی زیرِ هم بنشینند.
import time

size = 2_000_000
py_list = list(range(size))
np_array = np.arange(size)

start = time.perf_counter()
doubled_list = [x * 2 for x in py_list]
list_time = time.perf_counter() - start

start = time.perf_counter()
doubled_array = np_array * 2
array_time = time.perf_counter() - start

print(f"list: {list_time:.3f} ثانیه")
print(f"array: {array_time:.3f} ثانیه")
print(f"نسبت: حدود {list_time / array_time:.0f} برابر")
list: 0.180 ثانیه
array: 0.004 ثانیه
نسبت: حدود 45 برابر

عددهای تو با این‌ها فرق می‌کنند — به ماشینِ Colab و شلوغیِ لحظه بستگی دارد. چیزی که ثابت است جهتش است: آرایه ده‌ها برابر سریع‌تر است.

ولی به آن خطِ np_array * 2 دقت کن، چون مهم‌تر از سرعت است: هیچ حلقه‌ای ننوشتیم. یک عملیات روی کلِ آرایه. این را در فصلِ بعد کامل می‌بینی و همان‌جاست که کارِ داده واقعاً شروع می‌شود.

📏 اندازه بگیر: این اولین باری است که در این دوره ادعایی دربارهٔ سرعت کردیم، و توجه کن که اندازه‌اش گرفتیم — نه اینکه بگوییم «numpy سریع‌تر است». همین الگو در کلِ دوره برقرار است: هر ادعایی که می‌شود امتحانش کرد، امتحان می‌شود. و توجه کن که عدد را گِرد گفتیم («حدود ۴۵ برابر»)، چون عددِ دقیق در هر اجرا فرق می‌کند و نوشتنِ سه رقمِ اعشار برایش، دقتی را ادعا می‌کند که وجود ندارد.

واژه‌های تازهٔ این فصل#

کلمه تلفظ به حروف فارسی یعنی چه
numpy نام‌پای کتابخانهٔ پایهٔ کارِ عددی در پایتون
ndarray اِن‌دی‌آری آرایهٔ n-بُعدیِ numpy؛ همهٔ عنصرها یک نوع دارند
shape شِیپ شکلِ آرایه به‌صورتِ tuple؛ اول ردیف، بعد ستون
dtype دی‌تایپ نوعِ عنصرهای آرایه
linspace لین‌اسپیس n عددِ هم‌فاصله بینِ دو مقدار، شاملِ هر دو سر

تمرین‌ها

اول خودت فکر کن یا امتحان کن — بعد اینجا را باز کن.

در فصل بعد#

حالا آرایه داری، ولی هنوز مثلِ فهرست با آن رفتار می‌کنی. فصلِ بعد قدرتِ اصلی را نشان می‌دهد: کارِ برداری. یاد می‌گیری روی کلِ یک آرایه یکجا عملیات انجام بدهی، شرط بگذاری و فقط عنصرهایی را که می‌خواهی جدا کنی — همه بدونِ یک خط حلقه. آن قاعدهٔ broadcast که در خطای بخشِ ۵ دیدی هم همان‌جا کامل توضیح داده می‌شود.

به آخر این فصل رسیدی!

اگر ساختی و جواب داد، این دکمه مال توست.