المسارات
/
Julia
Julia
/
المنهج
/
العشوائية
ال

العشوائية في Julia

تمرينان

نبذة عن العشوائية

تحتاج برامج كثيرة إلى قيم عشوائية (ظاهريًا) لمحاكاة أحداث من العالم الحقيقي.

ومن الأمثلة الشائعة والمألوفة:

  • رمي عملة معدنية: قيمة عشوائية من ('H', 'T').
  • رمي حجر نرد: عدد صحيح عشوائي من 1 إلى 6.
  • خلط مجموعة أوراق لعب: ترتيب عشوائي لمصفوفة من الأوراق.

يُعد توليد قيم عشوائية حقيقية باستخدام الحاسوب تحديًا تقنيًا صعبًا بشكل مفاجئ، لذلك قد ترى هذه النتائج يُشار إليها بأنها «عشوائية زائفة».

مهم: لا يتناول هذا المفهوم الأعداد العشوائية الآمنة تشفيريًا، وهي تحدٍّ أصعب بكثير.

غير أن المكتبات المصممة جيدًا، مثل وحدة Random في المكتبة القياسية لـJulia، سريعة ومرنة وتعطي نتائج جيدة بما يكفي بوفرة لمعظم التطبيقات في النمذجة والمحاكاة والألعاب.

تقسم Julia وظائف العشوائية على عدة مواضع:

  • عدد قليل فقط من الدوال الأساسية لكن متعددة الاستخدامات في Base، وهي متاحة دائمًا.
  • مجموعة أوسع من الخيارات في وحدة Random.
  • وظائف أكثر تخصصًا في حزم تحتاج إلى تثبيت قبل الاستخدام (وهي غير متاحة في Exercism).

تعد 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 كرات خضراء، وأننا نسحب منه كرة بشكل عشوائي. وللحصول على كرة ثانية، هناك احتمالان:

  1. أن تعيد الكرة الأولى إلى الكيس وترجّ محتواه جيدًا قبل سحب كرة أخرى. يصبح عدد الكرات الآن كما كان (7)، و_تبقى نسبة الحمراء إلى الخضراء هي نفسها_.
  2. أن تضع الكرة الأولى على الطاولة قبل سحب كرة ثانية. الآن لم يبقَ في الكيس سوى 6 كرات، و_تعتمد نسبة الحمراء إلى الخضراء على لون الكرة الأولى_.

الحالة الأولى مع الاستبدال، والثانية بدونه، و_نتائجهما مختلفة_.

ولمحاكاة أخذ العينات بدون استبدال في Julia، هناك خياران أو أكثر.

أبسطها (وهو الخيار الوحيد داخل Exercism) استخدام Random.shuffle() لوضع العناصر بترتيب عشوائي، ثم استخدام العناصر n الأولى. هذا مناسب للمسائل الصغيرة لكنه قد لا يتوسع جيدًا مع المجموعات الكبيرة: تحتاج shuffle إلى توليد المصفوفة كاملة، حتى لو أردت جزءًا صغيرًا منها فقط.

ولإجراء أخذ العينات مع الاستبدال «بشكل صحيح»، ثبّت حزمة StatsBase.jl. فهي توفّر الدالة sample() بمجموعة كاملة من الخيارات.

يمكننا أن نأمل بحق أن تُضاف وظائف مماثلة إلى Random في إصدار مستقبلي لتصبح جزءًا من المكتبة القياسية (اختُبرت أمثلة الكود في هذا المستند باستخدام Julia 1.11).

التعامل مع التوزيعات

حتى الآن ركّزنا على الحالات التي تكون فيها جميع النتائج متساوية الاحتمال. فعلى سبيل المثال، من المرجح بالقدر نفسه أن تُرجع rand(1:100) أي عدد صحيح من 1 إلى 100.

كثير من مواقف العالم الحقيقي أبعد ما تكون عن هذه البساطة. ونتيجة لذلك، أنشأ الإحصائيون تنوعًا واسعًا من distributions لوصف نتائج «العالم الحقيقي» رياضيًا.

التوزيعات المنتظمة

تُستخدم دالة rand() الموضحة أعلاه عندما تكون جميع الاحتمالات متساوية. ويسمى هذا توزيعًا [uniform][uniform-distribution].

التوزيع الغوسي

ويسمى أيضًا التوزيع «الطبيعي» أو المنحنى «على شكل الجرس»، وهو طريقة شائعة جدًا لوصف عدم الدقة في القيم المقيسة.

على سبيل المثال، افترض أن المصنع الذي تعمل فيه اشترى للتو 10,000 مسمار، يُفترض أن تكون متطابقة. وتريد ضبط روبوت المصنع للتعامل معها، فتزن عينة من 100 مسمار وتجد أن متوسط وزنها (أو mean) هو 4.731 غرام. ومن المستبعد جدًا أن يعني هذا أن وزنها جميعًا 4.731 غرام بالضبط. ربما تجد أن القيم تتراوح بين 4.627 و4.794 غرام لكنها تتجمع حول 4.731 غرام.

هذا هو Gaussian distribution، حيث تبلغ الاحتمالات ذروتها عند المتوسط وتتناقص بشكل متماثل على الجانبين (ومن هنا تسميته «على شكل الجرس»). ولمحاكاة هذا برمجيًا، نحتاج إلى طريقة لتحديد عرض المنحنى (فالمسامير الغالية عادة تتجمع حول المتوسط بشكل أضيق من المسامير الرخيصة!).

وبحكم العرف، يُعبَّر عن ذلك بـstandard deviation: قيم صغيرة لمنحنى حاد ضيق، وقيم كبيرة لمنحنى منخفض عريض. يعشق الرياضيون الحروف اليونانية، لذا نستخدم μ («مو») لتمثيل المتوسط وσ («سيغما») لتمثيل الانحراف المعياري. وبالتالي، إذا قرأت أن «95% من القيم تقع ضمن 2σ من μ» أو أن «بوزون هيغز رُصد بثقة 5 سيغما»، فإن مثل هذه التعليقات تتعلق بالانحراف المعياري.

سيكون لدينا المزيد لقوله عن هذا في مفهوم Statistics.

دالة randn()

هي اختصار لـ«العشوائية الطبيعية»، وتشبه النسخة العشرية من 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).

ومن الدوال ذات الصلة randsubseq لسحب عناصر باحتمال ثابت، وrandcycle للتبديلات الدورية. تتطلب هذه الدوال بعض المعرفة المتخصصة، فتحقق من التوثيق إن كانت تهمك.

البذور والخوارزميات

تتضمن Random عدة خوارزميات لمولّدات الأعداد العشوائية (RNG) بشكل قياسي، ويمكن لأي شخص يمتلك المهارات الرياضية المناسبة أن يضيف المزيد. وهذه الأمور تتجاوز نطاق هذا المستند بكثير!

ومن الأسباب الأكثر شيوعًا للتعامل مع مولّدات الأعداد العشوائية تحديد seed، وهو ما يجعل متتابعة المخرجات «العشوائية» قابلة لإعادة الإنتاج من تشغيل إلى آخر.

قابلية إعادة الإنتاج هذه لا تصلح في كود الإنتاج، لكنها قد تساعد في الاختبار وتصحيح الأخطاء.

حزم أخرى

خارج Exercism، هناك العديد من الحزم القابلة للتثبيت التي تتعلق بالعشوائية والاحتمالات والإحصاء. لمزيد من المعلومات، راجع مفهوم Statistics.

حزمة StatsBase.jl

معظم دوال StatsBase تقنية إلى حد كبير وليست ذات صلة بهذا المستند.

والاستثناء هو StatsBase.sample، الذي يوفر تطبيقًا كاملًا لأخذ العينات مع الاستبدال أو بدونه (راجع قسمًا سابقًا أعلاه). وهناك أيضًا دوال لأخذ العينات الموزونة (غير المنتظمة).

حزمة Distributions.jl

وُصف التوزيعان uniform وnormal (أو الغوسي) أعلاه.

تحتوي وحدة Random أيضًا على randexp لأخذ عينات من التوزيع الأسي، وهو مرتبط بـتوزيع بواسون (الشائع جدًا).

وللحصول على نطاق أوسع بكثير من الخيارات، هناك حزمة Distributions.jl لمن لديهم خلفية مناسبة في الإحصاء.

تعديل عبر GitHub يفتح الرابط في نافذة أو علامة تبويب جديدة

تعلّم العشوائية

التدريب مقفل

افتح 1 تمرين إضافي لممارسة العشوائية