تص

تصادفی بودن در Julia

2 تمرین

درباره‌ی تصادفی بودن

بسیاری از برنامه‌ها برای شبیه‌سازی رویدادهای دنیای واقعی به مقادیر تصادفی (ظاهراً تصادفی) نیاز دارند.

مثال‌های رایج و آشنا عبارت‌اند از:

  • انداختن سکه: مقداری تصادفی از ('H', 'T').
  • انداختن تاس: عددی صحیح و تصادفی از ۱ تا ۶.
  • درهم‌آمیزی یک دسته کارت: ترتیبی تصادفی از فهرست کارت‌ها.

تولید مقادیر واقعاً تصادفی با رایانه چالشی فنی و به‌طرز غافلگیرکننده‌ای دشوار است، از این رو ممکن است ببینید به این نتایج «شبه‌تصادفی» می‌گویند.

مهم: این مفهوم اعداد تصادفی امن از نظر رمزنگاری را پوشش نمی‌دهد؛ اینها چالشی بسیار دشوارترند.

با این حال، کتابخانه‌های خوش‌ساخت مانند ماژول Random در کتابخانه‌ی استاندارد Julia سریع و انعطاف‌پذیرند و نتایجی می‌دهند که برای بیشتر کاربردها در مدل‌سازی، شبیه‌سازی و بازی‌ها به‌اندازه‌ی کافی خوب است.

Julia قابلیت‌های تصادفی را در چند جا تقسیم کرده است:

  • فقط چند تابع پایه اما بسیار همه‌کاره در Base که همیشه در دسترس‌اند.
  • گستره‌ی وسیع‌تری از گزینه‌ها در ماژول Random.
  • قابلیت‌های تخصصی‌تر در بسته‌هایی که باید پیش از استفاده نصب شوند (و در Exercism در دسترس نیستند).

Random بخشی از کتابخانه‌ی استاندارد است و احتمالاً از پیش نصب شده، اما باید using Random را در بالای برنامه‌تان اضافه کنید تا محتویات آن به فضای نام بیاید.

تابع rand()

کاری که این تابع انجام می‌دهد به آرگومان‌هایی بستگی دارد که به آن می‌دهید. گزینه‌های زیادی وجود دارد.

بدون آرگومان، یک عدد اعشاری بین ۰ (شامل ۰) و ۱ تولید می‌کند. این یک توزیع uniform است که در آن همه‌ی مقادیر به یک اندازه محتمل‌اند، همان‌طور که در بخش «کار با توزیع‌ها» در ادامه بحث می‌شود.

یک آرگومان صحیح، برداری به همان طول تولید می‌کند.

julia> rand()
0.10261774967264703

julia> rand(5)
5-element Vector{Float64}:
 0.24134501977563894
 0.5664193284851202
 0.9804412082089355
 0.6229551330613335
 0.47589221741904664

برای بازه‌ای متفاوت، فقط نتیجه را به‌شکل مناسب جابه‌جا و مقیاس کنید.

مثال زیر برای تفریق از پخش‌سازی استفاده می‌کند که در مفهوم عملیات برداری پوشش داده شده است. عملگر .- به‌سادگی این حساب را روی هر عنصر بردار اعمال می‌کند.

# numbers between -1.0 and +1.0
julia> (rand(5) .- 0.5) * 2
5-element Vector{Float64}:
 -0.5303906759076336
  0.9635682226775855
 -0.048823697086981754
  0.465842804648374
  0.9880834344780736

اگر تنها آرگومان یک نوع داده باشد، rand از typemin و typemax به‌عنوان کران‌ها استفاده می‌کند. احتمالاً این چیزی نیست که می‌خواهید!

برای اعداد صحیح تصادفی، می‌توانیم یک بازه بدهیم و به‌صورت اختیاری مشخص کنیم چند مقدار تولید شود.

julia> rand(Int64)
-9159538335234594326 # not very useful

julia> rand(1:10, 5)
5-element Vector{Int64}:
 1
 1
 1
 4
 7

در مثال rand(1:10, 5) بالا توجه کنید که مقادیری تکراری وجود دارد (به‌طور تصادفی)، چون هر انتخاب از دیگری مستقل است. این همان «نمونه‌گیری با جایگذاری» است که در ادامه با جزئیات بیشتری بحث می‌شود.

برای مقادیر اعشاری در یک بازه، معمولاً باید اندازه‌ی گام را مشخص کنید. در غیر این صورت گام به‌طور پیش‌فرض ۱٫۰ خواهد بود که به‌ندرت مفید است.

julia> rand(2.4:0.01:3.2, 4)
4-element Vector{Float64}:
 3.19
 2.53
 3.14
 3.13

به‌جای آن، یک آرایه یا تاپل بدهید تا rand یک عنصر تصادفی برگرداند:

julia> rand([4, 9, 16, 25])
16

# coin flip
julia> rand(['H', 'T'])
'H': ASCII/Unicode U+0048 (category Lu: Letter, uppercase)

# mixed types in tuple
julia> rand( (1, 3.2, "name"), 2 )
2-element Vector{Any}:
 1
  "name"

نمونه‌گیری با جایگذاری یا بدون جایگذاری

تصور کنید کیسه‌ای داریم که ۳ توپ قرمز و ۴ توپ سبز در آن است و به‌طور تصادفی توپی را از کیسه بیرون می‌کشیم. برای برداشتن توپ دوم، دو حالت وجود دارد:

  1. توپ اول را به کیسه برگردانید و پیش از بیرون کشیدن توپ بعدی، همه را خوب تکان دهید. تعداد توپ‌ها حالا مانند قبل است (۷) و نسبت توپ‌های قرمز به سبز هم تغییری نکرده است.
  2. توپ اول را روی میز بگذارید و بعد توپ دوم را بیرون بکشید. حالا فقط ۶ توپ در کیسه است و نسبت قرمز به سبز به رنگ توپ اول بستگی دارد.

حالت ۱ با جایگذاری است، حالت ۲ بدون جایگذاری، و این دو نتایج متفاوتی می‌دهند.

برای شبیه‌سازی نمونه‌گیری بدون جایگذاری در Julia، چند گزینه وجود دارد.

ساده‌ترین راه (و در Exercism تنها گزینه) این است که با Random.shuffle() عناصر را به ترتیبی تصادفی بچینید و سپس از n عنصر اول استفاده کنید. این برای مسائل کوچک خوب است، اما ممکن است برای مجموعه‌های بزرگ مقیاس‌پذیر نباشد: shuffle باید کل آرایه را تولید کند، حتی اگر فقط کسر کوچکی از آن را بخواهید.

برای انجام درست نمونه‌گیری با جایگذاری، بسته‌ی StatsBase.jl را نصب کنید. این بسته تابع sample() را با گستره‌ای کامل از گزینه‌ها فراهم می‌کند.

می‌توان امیدوار بود که قابلیت‌های مشابه در نسخه‌ای آینده به Random افزوده شود تا بخشی از کتابخانه‌ی استاندارد شود (نمونه‌کدهای این سند با Julia 1.11 آزمایش شده‌اند).

کار با توزیع‌ها

تا اینجا روی حالت‌هایی تمرکز کرده‌ایم که همه‌ی نتایج به یک اندازه محتمل‌اند. برای نمونه، rand(1:100) به یک اندازه احتمال دارد هر عدد صحیحی از ۱ تا ۱۰۰ را بدهد.

بسیاری از موقعیت‌های دنیای واقعی بسیار پیچیده‌تر از این هستند. در نتیجه، آماردانان انواع گوناگونی از distributions را ساخته‌اند تا نتایج «دنیای واقعی» را به‌شکل ریاضی توصیف کنند.

توزیع‌های یکنواخت

تابع rand() که در بالا توصیف شد، زمانی به کار می‌رود که همه‌ی احتمال‌ها برابر باشند. به این، توزیع [uniform][uniform-distribution] می‌گویند.

توزیع گاوسی

به آن توزیع «نرمال» یا منحنی «زنگوله‌ای» هم می‌گویند و شیوه‌ای بسیار رایج برای توصیف عدم دقت در مقادیر اندازه‌گیری‌شده است.

برای نمونه، فرض کنید کارخانه‌ای که در آن کار می‌کنید به‌تازگی ۱۰٬۰۰۰ پیچ خریده است که باید همسان باشند. می‌خواهید ربات کارخانه را برای جابه‌جایی آن‌ها آماده کنید، پس نمونه‌ای ۱۰۰تایی را وزن می‌کنید و می‌بینید که میانگین (یا mean) وزنشان ۴٫۷۳۱ گرم است. این به‌احتمال بسیار زیاد به این معنا نیست که وزن همه‌ی آن‌ها دقیقاً ۴٫۷۳۱ گرم است. شاید ببینید که مقادیر از ۴٫۶۲۷ تا ۴٫۷۹۴ گرم در نوسان‌اند اما حول ۴٫۷۳۱ گرم جمع شده‌اند.

این همان Gaussian distribution است که در آن احتمال‌ها در میانگین به اوج می‌رسند و در دو طرف به‌شکل متقارن کاهش می‌یابند (از این رو «زنگوله‌ای»). برای شبیه‌سازی این در نرم‌افزار، به راهی نیاز داریم که پهنای منحنی را مشخص کند (معمولاً پیچ‌های گران‌تر بیش از پیچ‌های ارزان حول میانگین جمع می‌شوند!).

طبق قرارداد، این کار با standard deviation انجام می‌شود: مقادیر کوچک برای منحنی تیز و باریک و مقادیر بزرگ برای منحنی کوتاه و پهن. ریاضیدانان عاشق حروف یونانی‌اند، پس ما از μ («مو») برای نمایش میانگین و از σ («سیگما») برای نمایش انحراف معیار استفاده می‌کنیم. بنابراین اگر بخوانید که «۹۵٪ مقادیر در فاصله‌ی ۲σ از μ قرار دارند» یا «بوزون هیگز با اطمینان ۵ سیگما شناسایی شده است»، چنین جمله‌هایی به انحراف معیار مربوط می‌شوند.

در مفهوم Statistics درباره‌ی این بیشتر خواهیم گفت.

تابع randn()

کوتاه‌شده‌ی «random normal»، این تابع شبیه گونه‌ی اعشاری rand() است، با این تفاوت که مقادیر با توزیع گاوسی با میانگین ۰ و انحراف معیار ۱ پخش می‌شوند.

باز هم ممکن است بخواهید خروجی خام randn را برای انحراف معیار مقیاس کنید و برای میانگین جابه‌جا کنید. مثال زیر آن را به میانگین ۳۰ و انحراف معیار ۵ تبدیل می‌کند.

julia> raw = randn(5)
5-element Vector{Float64}:
  3.0762588867281475
  1.5101100620253902
 -0.5914858221637778
  0.684175554069735
 -0.8416433926114673

julia> raw * 5 .+ 30
5-element Vector{Float64}:
 45.38129443364074
 37.55055031012695
 27.04257088918111
 33.420877770348675
 25.791783036942665

از روی خروجی به‌سختی می‌توان فهمید که خروجی خام بیش از یک توزیع یکنواخت نزدیک صفر جمع می‌شود. اگر شک دارید، ۱۰۰۰ مقدار یا بیشتر تولید کنید و آن‌ها را رسم کنید تا آشکارتر شود.

ماژول Random

این ماژول رده‌ی بعدی قابلیت‌ها را در خود دارد که برای کمک به کوچک نگه داشتن پیکربندی پیش‌فرض Julia از Base بیرون گذاشته شده‌اند.

Random نسخه‌های تغییردهنده‌ی rand! و randn! را به rand و randn در Base می‌افزاید.

افزوده‌ی مفیدی هم randstring است که رشته‌هایی با طول داده‌شده تولید می‌کند. به‌طور پیش‌فرض، این تابع از حروف بزرگ و کوچک به‌همراه رقم‌های ۰ تا ۹ استفاده می‌کند، اما می‌توان گزینه‌های دیگری هم مشخص کرد.

julia> using Random

julia> randstring(20)
"BoJnIxrS33pJiWggXZQV"

به‌علاوه، تابع bitrand هم وجود دارد که یک BitArray تصادفی با طول مشخص تولید می‌کند.

julia> bitrand(5)
julia> bitrand(5)
5-element BitVector:
 1
 1
 0
 0
 1

درهم‌آمیزی‌ها و جایگشت‌ها

برای درهم‌آمیزی تصادفی عناصر یک Vector، تابع shuffle را داریم؛ همچنین shuffle! برای تغییر بردار ورودی در همان جا.

julia> v = ['A', '1', '2', 'J', 'Q', 'K'];

julia> shuffle(v)
6-element Vector{Char}:
 'K': ASCII/Unicode U+004B (category Lu: Letter, uppercase)
 '1': ASCII/Unicode U+0031 (category Nd: Number, decimal digit)
 'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
 'J': ASCII/Unicode U+004A (category Lu: Letter, uppercase)
 '2': ASCII/Unicode U+0032 (category Nd: Number, decimal digit)
 'Q': ASCII/Unicode U+0051 (category Lu: Letter, uppercase)

# shuffles are random:
julia> shuffle(v)
6-element Vector{Char}:
 '2': ASCII/Unicode U+0032 (category Nd: Number, decimal digit)
 'K': ASCII/Unicode U+004B (category Lu: Letter, uppercase)
 'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
 'Q': ASCII/Unicode U+0051 (category Lu: Letter, uppercase)
 'J': ASCII/Unicode U+004A (category Lu: Letter, uppercase)
 '1': ASCII/Unicode U+0031 (category Nd: Number, decimal digit)

گاهی داشتن اندیس‌های درهم‌آمیزشده به‌جای خود عناصر مفید است. برای این کار از randperm(n) استفاده کنید که در آن n طول دنباله است.

julia> randperm(6)
6-element Vector{Int64}:
 6
 2
 4
 1
 3
 5

در واقع، مثال بالا همان نتایج shuffle(1:6) را می‌دهد.

توابع مرتبط شامل randsubseq برای بیرون کشیدن عناصر با احتمال ثابت و randcycle برای جایگشت‌های چرخه‌ای است. این‌ها به دانش تخصصی نیاز دارند، پس اگر برایتان جالب است مستندات را ببینید.

دانه‌ها و الگوریتم‌ها

چندین الگوریتم مولد اعداد تصادفی (RNG) به‌طور استاندارد در Random گنجانده شده است و هر کسی با توانایی ریاضی مناسب می‌تواند الگوریتم‌های بیشتری اضافه کند. چنین چیزهایی بسیار فراتر از دامنه‌ی این سند است!

دلیل رایج‌تر برای کار با مولدهای اعداد تصادفی، مشخص کردن یک seed است که باعث می‌شود دنباله‌ی خروجی‌های «تصادفی» از یک اجرا به اجرای بعدی تکرارپذیر شود.

چنین تکرارپذیری در کد تولیدی مناسب نیست، اما می‌تواند در آزمون و اشکال‌زدایی کمک کند.

بسته‌های دیگر

بیرون از Exercism، بسته‌های نصب‌شدنی بسیاری در ارتباط با تصادفی‌بودن، احتمال و آمار وجود دارد. برای اطلاعات بیشتر، مفهوم Statistics را ببینید.

بسته‌ی StatsBase.jl

بیشتر توابع StatsBase کاملاً تخصصی‌اند و به این سند مربوط نمی‌شوند.

استثنا تابع StatsBase.sample است که پیاده‌سازی کاملی از نمونه‌گیری با جایگذاری یا بدون جایگذاری فراهم می‌کند (بخش پیشین را در بالا ببینید). توابعی هم برای نمونه‌گیری وزنی (غیریکنواخت) وجود دارد.

بسته‌ی Distributions.jl

توزیع‌های uniform و normal (یا گاوسی) در بالا توصیف شدند.

ماژول Random همچنین randexp را برای نمونه‌گیری از توزیع نمایی در خود دارد که با توزیع پواسون (بسیار رایج) مرتبط است.

برای گستره‌ای بسیار وسیع‌تر از گزینه‌ها، بسته‌ی Distributions.jl برای کسانی است که پیشینه‌ی مناسبی در آمار دارند.

ویرایش از طریق GitHub این پیوند در پنجره یا زبانه‌ی جدیدی باز می‌شود

تصادفی بودن را یاد بگیرید

تمرین کردن قفل شده است

برای تمرین تصادفی بودن قفل 1 تمرین دیگر را باز کنید