تحتاج برامج كثيرة إلى قيم عشوائية (ظاهريًا) لمحاكاة أحداث من العالم الحقيقي.
ومن الأمثلة الشائعة والمألوفة:
('H', 'T').يُعدّ توليد قيم عشوائية حقيقية باستخدام الحاسوب تحديًا تقنيًا صعبًا على نحو مفاجئ، لذلك قد ترى هذه النتائج مسمّاة «شبه عشوائية».
مهم: لا يغطي هذا المفهوم الأعداد العشوائية الآمنة تشفيريًا، وهي تحدٍّ أصعب بكثير.
غير أن المكتبات جيدة التصميم، مثل وحدة Random في مكتبة Julia القياسية، سريعة ومرنة، وتعطي نتائج جيدة بما يكفي لغالبية التطبيقات في النمذجة والمحاكاة والألعاب.
تقسم Julia وظائف العشوائية على عدة مواضع:
Base، وهي متاحة دائمًا.Random.وحدة Random جزء من المكتبة القياسية ويُرجّح أنها مثبّتة مسبقًا، لكنك ستحتاج إلى إضافة using Random في أعلى برنامجك لجلب محتوياتها إلى نطاق الأسماء.
rand()
يعتمد ما تفعله هذه الدالة على الوسائط التي تمررها إليها. والخيارات كثيرة.
عند عدم تمرير أي وسائط، تُولّد عددًا عشريًا بين 0 (ضمنًا) و1.
هذا توزيع 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) أعلاه، لاحظ وجود قيم متكررة (بمحض المصادفة)، لأن كل اختيار مستقل.
هذا ما يُسمى «أخذ العيّنات مع الاستبدال»، وسنناقشه بمزيد من التفصيل أدناه.
أما القيم العشرية ضمن مجال، فستحتاج عادةً إلى تحديد حجم الخطوة. وإلا فستكون الخطوة الافتراضية 1.0، وهو أمر نادرًا ما يكون مفيدًا.
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"
تخيّل أن لدينا كيسًا يحتوي على 3 كرات حمراء و4 كرات خضراء، ونسحب منه كرة عشوائيًا. وللحصول على كرة ثانية، هناك احتمالان:
الحالة الأولى مع الاستبدال، والثانية بدون استبدال، و_لكل منهما نتائج مختلفة_.
ولمحاكاة أخذ العيّنات بدون استبدال في Julia، أمامك خياران أو نحو ذلك.
أبسط الخيارات (وخيارك الوحيد داخل Exercism) هو استخدام Random.shuffle() لترتيب العناصر ترتيبًا عشوائيًا، ثم استخدام العناصر n الأولى.
وهذا جيد للمسائل الصغيرة، لكنه قد لا يتوسّع جيدًا إلى المجموعات الكبيرة: إذ تحتاج shuffle إلى توليد المصفوفة كاملة، حتى لو كنت تريد جزءًا صغيرًا منها فقط.
ولأخذ العيّنات مع الاستبدال «على نحو سليم»، ثبّت حزمة StatsBase.jl.
فهي توفّر الدالة sample() بمجموعة كاملة من الخيارات.
ويمكننا أن نأمل بمعقولية أن تُضاف وظائف مشابهة إلى وحدة Random في إصدار مستقبلي، لتصبح جزءًا من المكتبة القياسية (اختُبرت أمثلة الكود في هذا المستند باستخدام Julia 1.11).
ركّزنا حتى الآن على الحالات التي تكون فيها جميع النتائج متساوية الاحتمال.
فمثلًا، rand(1:100) متساوية الاحتمال في إعطاء أي عدد صحيح من 1 إلى 100.
لكنّ مواقف كثيرة في العالم الحقيقي أقل بساطة بكثير من ذلك.
ولهذا أنشأ الإحصائيون مجموعة واسعة من distributions لوصف نتائج «العالم الحقيقي» رياضيًا.
تُستخدم الدالة rand() الموصوفة أعلاه عندما تتساوى جميع الاحتمالات.
ويُسمى هذا توزيع uniform.
يُسمى أيضًا التوزيع «الطبيعي» أو منحنى «شكل الجرس»، وهو وسيلة شائعة جدًا لوصف عدم الدقة في القيم المقيسة.
لنفترض، على سبيل المثال، أن المصنع الذي تعمل فيه اشترى للتو 10,000 مسمار برغي يُفترض أنها متطابقة.
وتريد ضبط روبوت المصنع للتعامل معها، فتزن عيّنة من 100 مسمار وتجد أن متوسط وزنها (أو mean) هو 4.731 غرام.
ومن غير المرجّح للغاية أن يعني هذا أنها كلها تزن 4.731 غرام بالضبط.
ربما تجد أن القيم تتراوح من 4.627 إلى 4.794 غرام، لكنها تتجمّع حول 4.731 غرام.
هذا هو Gaussian distribution، حيث تبلغ الاحتمالات ذروتها عند المتوسط وتتناقص بشكل متماثل على الجانبين (ومن هنا وصف «شكل الجرس»).
ولمحاكاة ذلك برمجيًا، نحتاج إلى طريقة لتحديد عرض المنحنى (فالمسامير الغالية عادةً تتجمّع حول المتوسط بقرب أكبر من المسامير الرخيصة!).
وجرى العرف على أن يُفعل ذلك بـ standard deviation: قيم صغيرة لمنحنى حاد ضيّق، وقيم كبيرة لمنحنى منخفض عريض.
يعشق الرياضيون الحروف اليونانية، لذا نستخدم μ ('mu') لتمثيل المتوسط وσ ('sigma') لتمثيل الانحراف المعياري.
وبالتالي، إذا قرأت أن «95% من القيم تقع ضمن 2σ من μ» أو أن «بوزون هيغز رُصد بثقة 5-sigma»، فإن مثل هذه التعليقات تتعلق بالانحراف المعياري.
وسيكون لدينا المزيد لنقوله عن هذا في مفهوم Statistics.
randn()
هي اختصار لعبارة «random normal»، وتشبه النسخة العشرية من rand()، إلا أن القيم فيها موزّعة توزيعًا غاوسيًا بمتوسط 0 وانحراف معياري 1.
ومرة أخرى، قد ترغب في تحجيم المخرجات الخام من randn بحسب الانحراف المعياري، وإزاحتها بحسب المتوسط.
والمثال أدناه يحوّلها إلى متوسط 30 وانحراف معياري 5.
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
يصعب أن تتبيّن من النظر إلى المخرجات أن المخرجات الخام تتجمّع قرب الصفر أكثر مما في التوزيع المنتظم. وإن كنت تشك في ذلك، فولّد 1000 قيمة أو أكثر وارسمها لتوضيح الأمر.
Random
تحتوي هذه الوحدة على الطبقة التالية من الوظائف، وقد استُبعدت من Base للمساعدة في تصغير حجم الإعداد الافتراضي لـ Julia.
وتضيف وحدة Random إلى rand وrandn في Base نسخًا معدِّلة، هما rand! وrandn!.
ومن الإضافات المفيدة الدالة randstring، التي تولّد سلسلة نصية بطول معيّن.
وهي تستخدم افتراضيًا حروفًا كبيرة وصغيرة إضافة إلى الأرقام من 0 إلى 9، لكن يمكن تحديد خيارات أخرى.
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-" ثم رقم من 1000 إلى 9999 (شاملًا الطرفين).
نفّذ الدالة random_ship_registry_number التي تُرجع رقم تسجيل عشوائيًا لسفينة فضاء.
julia> random_ship_registry_number()
"NCC-1947"
ما فائدة سجل لا يتضمن تواريخ؟
التاريخ النجمي هو عدد عشري. تجري مغامرات سفينة Starship Enterprise في الموسم الأول من The Next Generation بين التاريخين النجميين 41000.0 و42000.0. يرمز "4" إلى القرن الرابع والعشرين، و"1" إلى الموسم الأول.
نفّذ الدالة random_stardate التي تُرجع عددًا عشريًا بين 41000.0 (شاملًا) و42000.0.
julia> random_stardate()
41458.15721310934
كانت هناك شكاوى من كثرة المنازل العشرية في التواريخ النجمية، لذا قررت ماري تقريب السجلات إلى منزلة عشرية واحدة.
رغم أنه يمكن فعل ذلك بتقريب القيم الناتجة من random_stardate()، فإنك تدرك أن هناك أسلوبًا أبسط بكثير.
نفّذ الدالة random_stardate_v2 التي تُرجع عددًا عشريًا بين 41000.0 و42000.0 (شاملًا)، بمنزلة عشرية واحدة.
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() ليعمل مع كائنات أخرى قابلة للتكرار، مثل الصفوف أو المجموعات؟
لا توجد اختبارات لهذا الجزء.
سجّل في Exercism لتتعلّم وتتقن Julia عبر 35 مفهومًا128 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.