بسیاری از برنامهها برای شبیهسازی رویدادهای دنیای واقعی به مقادیر تصادفی (بهظاهر تصادفی) نیاز دارند.
نمونههای رایج و آشنا عبارتاند از:
('H', 'T').تولید مقادیر کاملاً تصادفی با رایانه چالشی فنی و بهطرز شگفتآوری دشوار است، بنابراین ممکن است ببینید که به این نتایج «شبهتصادفی» میگویند.
مهم: این مفهوم اعداد تصادفیِ امن از نظر رمزنگاری را پوشش نمیدهد، که چالشی بسیار دشوارتر است.
با این حال، کتابخانههای خوبطراحیشده مانند ماژول Random در کتابخانهی استاندارد Julia سریع و انعطافپذیرند و نتایجی میدهند که برای بیشتر کاربردهای مدلسازی، شبیهسازی و بازیها بهاندازهی کافی خوب است.
Julia قابلیتهای تصادفی را در چند جا تقسیم کرده است:
Base، که همیشه در دسترساند.Random.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"
تصور کنید کیسهای داریم که ۳ توپ قرمز و ۴ توپ سبز در آن است، و بهصورت تصادفی توپی از کیسه بیرون میکشیم. برای گرفتن توپ دوم، دو حالت وجود دارد:
حالت ۱ با جایگذاری است، حالت ۲ بدون جایگذاری، و نتیجههای متفاوتی میدهند.
برای شبیهسازی نمونهگیری بدون جایگذاری در Julia، چند گزینه وجود دارد.
سادهترین راه (و در Exercism تنها گزینه) این است که از Random.shuffle() استفاده کنید تا عنصرها را به ترتیب تصادفی بچیند، و سپس از n عنصر اول استفاده کنید.
این برای مسائل کوچک خوب است اما ممکن است برای مجموعههای بزرگ مقیاسپذیر نباشد: shuffle باید کل آرایه را تولید کند، حتی اگر فقط کسر کوچکی از آن را بخواهید.
برای انجام نمونهگیری با جایگذاری بهشکل «درست»، بستهی StatsBase.jl را نصب کنید.
این بسته تابع sample() را با گسترهای کامل از گزینهها فراهم میکند.
میتوانیم بهطور منطقی امیدوار باشیم که قابلیتهای مشابه در نسخههای آینده به Random اضافه شود تا بخشی از کتابخانهی استاندارد شود (نمونهکدهای این سند با Julia ۱٫۱۱ آزمایش شدهاند).
تا اینجا روی حالتهایی تمرکز کردهایم که همهی نتایج به یک اندازه محتملاند.
برای مثال، rand(1:100) به یک اندازه احتمال دارد هر عدد صحیح از ۱ تا ۱۰۰ را بدهد.
بسیاری از موقعیتهای دنیای واقعی بهمراتب پیچیدهتر از ایناند.
در نتیجه، آماردانان انواع گستردهای از distributions را ساختهاند تا نتایج «دنیای واقعی» را بهصورت ریاضی توصیف کنند.
تابع rand() که در بالا توصیف شد، وقتی همهی احتمالها برابرند به کار میرود.
به این یک توزیع uniform میگویند.
به آن توزیع «نرمال» یا منحنی «زنگولهای» هم میگویند؛ این روشی بسیار رایج برای توصیف عدم دقت در مقادیر اندازهگیریشده است.
برای مثال، فرض کنید کارخانهای که در آن کار میکنید بهتازگی ۱۰٬۰۰۰ پیچ خریده است که باید یکسان باشند.
میخواهید ربات کارخانه را برای کار با آنها آماده کنید، پس نمونهای ۱۰۰تایی را وزن میکنید و میبینید میانگین (یا 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
این ماژول سطح بعدی قابلیتها را در خود دارد که از Base حذف شدهاند تا به کوچک نگه داشتن اندازهی پیکربندی پیشفرض Julia کمک کنند.
Random توابع rand و randn در Base را با نسخههای تغییردهنده، یعنی rand! و randn!، تکمیل میکند.
افزودنی مفید 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) را میدهد.
مری طرفدار پروپاقرص مجموعهی تلویزیونی Star Trek: The Next Generation است. او اغلب بازیهای نقشآفرینی قلموکاغذی انجام میدهد که در آنها خودش و دوستانش خود را خدمهی سفینهی Starship Enterprise جا میزنند. شخصیت مری، کاپیتان پیکارد است؛ یعنی او باید دفترچهی کاپیتان را نگه دارد. او بخش خلاقانهی بازی را دوست دارد، اما خوشش نمیآید دادههای تصادفی را همانجا و در لحظه تولید کند.
به مری کمک کنید و برای دادههایی که معمولاً در دفترچهی کاپیتان ظاهر میشوند، تولیدکنندههای تصادفی بسازید.
سفینهی Starship Enterprise در سفرهایش با سیارههای زیادی روبهرو میشود. سیارهها در دنیای Star Trek بر اساس ویژگیهایشان به دستههایی تقسیم میشوند. برای مثال، زمین یک سیارهی کلاس M است. همهی کلاسهای سیارهای ممکن عبارتاند از: D، H، J، K، L، M، N، R، T و Y.
تابع random_planet را پیادهسازی کنید. این تابع باید یکی از کلاسهای سیارهای را بهصورت تصادفی برگرداند.
julia> random_planet_class()
'K'
Enterprise (با شمارهی ثبت NCC-1701) تنها سفینهی فضاییای نیست که در حال پرواز است! وقتی با سفینهی فضایی دیگری ملاقات میکند، مری باید شمارهی ثبت آن سفینه را یادداشت کند.
شمارههای ثبت با پیشوند «NCC-» شروع میشوند و سپس عددی از ۱۰۰۰ تا ۹۹۹۹ به کار میرود (شامل هر دو سر بازه).
تابع random_ship_registry_number را پیادهسازی کنید که یک شمارهی ثبت تصادفی سفینه را برمیگرداند.
julia> random_ship_registry_number()
"NCC-1947"
اگر تاریخ نداشته باشد، دفترچه به چه کار میآید؟
«استاردیت» یک عدد اعشاری است. ماجراهای سفینهی Starship Enterprise در فصل اول The Next Generation بین استاردیتهای ۴۱۰۰۰٫۰ و ۴۲۰۰۰٫۰ رخ میدهند. «۴» به قرن ۲۴ اشاره دارد و «۱» به فصل اول.
تابع random_stardate را پیادهسازی کنید که عددی اعشاری بین ۴۱۰۰۰٫۰ (شامل) و ۴۲۰۰۰٫۰ برمیگرداند.
julia> random_stardate()
41458.15721310934
شکایتهایی شده که استاردیتها رقمهای اعشاری زیادی دارند، بنابراین مری تصمیم میگیرد دفترچه را تا یک رقم اعشار گرد کند.
هرچند میتوان این کار را با گرد کردن مقادیر random_stardate() انجام داد، متوجه میشوید که راه بسیار سادهتری هم وجود دارد.
تابع random_stardate_v2 را پیادهسازی کنید که عددی اعشاری بین ۴۱۰۰۰٫۰ و ۴۲۰۰۰٫۰ (شامل هر دو سر) و با یک رقم اعشار برمیگرداند.
julia> random_stardate_v2()
41732.6
در یکی از نسخههای بازی، تعامل با سفینههای فضایی دیگر مطرح است و مری به چند سفینه نیاز دارد.
فهرست کاملی از سفینههای ممکن وجود دارد، اما در هر بازی فقط به چند سفینه از این فهرست نیاز است و باید بهصورت تصادفی انتخاب شوند.
تابع pick_starships(starships, number_needed) را پیادهسازی کنید؛ این تابع برداری از شمارههای ثبت یکتای سفینهها میگیرد و برداری به طول number_needed برمیگرداند. مقادیر نتیجه باید از starships گرفته شوند و هیچ تکراری نداشته باشند.
julia> pick_starships(["NCC-5011", "NCC-1228", "NCC-7039", "NCC-3978", "NCC-1476"], 2)
2-element Vector{String}:
"NCC-1228"
"NCC-5011"
ما آرگومان starships را به یک بردار محدود کردیم.
چطور pick_starships() را طوری پیادهسازی میکنید که با سایر اشیای قابل پیمایش، مثل تاپلها یا مجموعهها، کار کند؟
برای این بخش آزمونی وجود ندارد.