من قبل Bruno Dias
بفضل كرم داعمي PROCJAM على Kickstarter، أمكن لي القدوم لكتابة درس عن مبادئ Improv. هذا درس شبه متقدم، ويتطلب فهمه أن يكون القارئ بارعاً في محررات النصوص ولغة برمجة JavaScript وأدوات واجهة سطر الأوامر، ولكنني لن أخوض في أي شيء بالغ التعقيد (على ما أأمل).
مكتبة Improv هي أداة تمكن من توليد النصوص إجرائياً عبر استخدام ملفات تدعى ملفات الإملاء "grammar". ملفات الإملاء عبارة عن مجموعة من القواعد التي تعرّف كيفية كتابة نص من خلال تجميعه ضمن أجزاء أبسط. العائق الأساسي لاستخدام ملفات الإملاء أن صنعها مرهق؛ يجب أن تكتب باليد أو تولّد من وسيلة أخرى (كتوليدها من مجموعة من البيانات المنظمة). ولكن يمكن لملفات الإملاء أن تولّد نصوصاً ذات نظام منطقي ودون تغيّر، واستخدام ملفات الإملاء أسهل للفهم (وللحصول على نتائج جيدة) من سلسلات ماركوف markov chains أو النص المتنبئ predictive text أو الشبكات العصبونية neural networks أو أياً من تقنيات حقل "تعلم الآلة" الحديثة.
يعود أصل هيكل Improv الأساسي إلى أداة تدعى Tracery من قبل (كايت كامبتون) وإلى منهجية صممتها (إيميلي شورت) لكتابة قصة "حواليات أهل الباريغ". تم تصميم Improv من أجل لعبة Voyageur بحيث تكون الأداة قادرة على توليد فقرات ذات أوصاف متنوعة للمناطق والأشياء ضمن اللعبة، وإعطائها وصفاً غير متناقض وذو متغيرات معقدة عديدة. Voyageur هي لعبة عن التجارة واستكشاف الفضاء حيث يتنقل اللاعبون بين الكواكب. في Voyageur، ويملك كل كوكب عوامل مستقلة كبيئة وأيديولوجيا واقتصاد خاصين به؛ ويتم إعطاء الكوكب وصفاً يمسّ كل تلك العوامل.
نحن نحتاج بيئة يمكنها تشغيل JavaScript. وعملياً إن أكثر المشاريع التي سنريد استخدام Improv فيها هي في بيئات مشابهة للمتصفحات مثل Electron أو Cordova. ولكن يستطيع أيضاً Improv أن يعمل ضمن Node.js، وهو ما سنستخدمه في هذا الدرس من أجل تبسيط الأمور، سنحتاج Node بكل الأحوال لتنزيل نسخة من Improv عبر خدمة npm لإدارة وتحميل ملحقات node. لذا يمكنك إنشاء بيئة Improv بسيطة على حاسوب منزلٌ عليه Node.js (وباستخدام إما واجهة سطر الأوامر الافتراضية في Linux/MacOS أو Powershell على Windows) عبر كتابة التالي:
mkdir improv-tutorial
cd improv-tutorial
npm install fs-jetpack js-yaml improv
سوف يتذمر npm حول عدم وجود ملف package.json في مشروعك ولكنه سينزل ذلك الملف. قم الآن بإنشاء ملفي improv-tutorial.js و grammar.yaml في مجلد مشروعك.
هذا نص بسيط لتشغيل Improv:
// إيجاد الملحقات
const Improv = require('improv');
const yaml = require('js-yaml');
const fs = require('fs-jetpack');
// تحميل بياناتنا من ملف
const grammarData = yaml.load(fs.read('grammar.yaml'));
// إنشاء كائن مولِّد من تلك البيانات
const generator = new Improv(grammarData, {
filters: [Improv.filters.mismatchFilter()],
reincorporate: true
});
// توليد نص وكتابته
console.log(generator.gen('root', {}));
Improv
هي
دالة إنشاء
"constructor"
محملة من مكتبة خارجية
(لذا يجب أن تستخدم مع عبارة
new
)
التي تنشأ وترجع كائناً مولّداً.
تقوم بأخذ
وسيطين
"parameters":
ملف إملائي
وكائن آخر لضبط إعداد المولّد.
يوجد الكثير من الخيارات هنا، لكن من أجل هذا الدرس سنستخدم فقط الفلترات
filters
وإعادة الدمج reincorporate
;
سأعود إلى معنى تلك العبارتين لاحقاً في الدرس.
الآن بمجرد كتابة
node improv-tutorial.js
يمكنك تشغيل هذا المشروع،
ولكن إن قمت بذلك فسيظهر خطأً لأن ملف
grammar.yaml
ليس موجوداً بعد. لذا سنقوم بإنشائه الآن.
Improv هي مكتبة مكتوبة في JavaScript، لذا فإن بيانات ملفات الإملاء هي كائنات في JavaScript ولكن ذات هيكل طبقي عميق يمكن أن يكرر نفسه. لتسهيل قراءة الملف وحفظ صحتي العقلية، أحب أن أكتبهم باستخدام YAML بدلاً من JavaScript. ملف إملائي بسيط يبدو كالتالي
root:
groups:
-
tags: []
phrases:
- "The [:prefix] [:name] is a [:class] commissioned in 1888"
prefix:
groups:
-
tags: []
phrases:
- "HMS"
name:
groups:
-
tags: []
phrases:
- "Invincible"
class:
groups:
-
tags: []
phrases:
- "cutter"
احفظ ذلك الملف باسم
grammar.yaml
ثم شغل النص مجدداً، يجب أن تحصل على هذا المنتج:
"The HMS Invincible is a cutter commissioned in 1888."
قد تتيه مما حدث الآن لذا دعني أن أوضح خطوة بخطوة: كل مفتاح في الكائن
(مثل
الجذر root
و
البداية prefix
)
هم
قواعد.
كل قاعدة تحتوي على
مجموعات
"groups"
(يوجد فقط واحدة في مثالنا هذا)،
وكل مجموعة تحوي لائحة من
الوصائف
tags
ولائحة من
العبارات
phrases.
كل عبارة هي كتلة مستقلة من نص قد تحوي على
توجيهات
directives
(على سبيل المثال [:prefix]
)
التي تشير إلى قاعدة أخرى.
هذه كيفية عمل
Improv:
نسأل الأداة بأن تولد
جذر root
القاعدة
(generator.gen('root', {})
في ملف
JavaScript).
يقوم
Improv
بالتحقق عن كل المجموعات من أجل تلك القاعدة؛ يوجد بالطبع قاعدة واحدة فقط في مشروعنا هذا.
توجد أولاً خطوة فلترة، وسنتحدث عنها بالتفصيل لاحقاً، تقوم باختيار المجموعات التي ستستخدمها. ثم تقوم بجمع
كل العبارات وتضعها في المجموعات السابقة وتختار عبارة عشوائياً.
يتم صنع نموذج من كل عبارة؛ ينظر
Improv
إلى التوجيهات المحاطة
[بقوسين مربعين].
التوجيهات البادئة بنقطتين منقوطتين، مثل
[:prefix]
،
هن زبدة توليد النصوص في هذه الأداة؛ عندما يلاقي
Improv
أحد تلك التوجيهات، إن وردت، فإنه يولّد تلك القاعدة.
لايوجد الآن شيء مفاجئ في مشروعنا؛ سنحصل على نفس المخرج كل مرة لأن جميع قواعدنا تحوي عبارة وحيدة فيهم. يمكننا إضافة المزيد من التنويعات بإضافة عبارات أخرى:
root:
groups:
-
tags: []
phrases:
- "The [:prefix] [:name] is a [:class] commissioned in [#1880-1910]"
prefix:
groups:
-
tags: []
phrases:
- "HMS"
name:
groups:
-
tags: []
phrases:
- "Invincible"
- "Unstoppable"
- "Indefatigable"
- "Inevitable"
- "Inerrant"
class:
groups:
-
tags: []
phrases:
- "cutter"
- "torpedo boat"
- "light cruiser"
- "heavy cruiser"
- "battleship"
وبهذه البساطة، أصبح لدينا تنويعات مختلفة:
The HMS Unstoppable is a battleship commissioned in 1898
The HMS Invincible is a cutter commissioned in 1891
The HMS Indefatigable is a light cruiser commissioned in 1906
لاحظ عبارة
[#1880-1910]
؛
تلك هي توجيه مميز يمكنه إنتاج رقم عشوائي بين 1880 و 1910.
لنقل أننا نريد تضمين كلاً من البارجات الحربية والمدنية في قواعد الإملاء. سنضيف تنويعات مدنية ذات أسماء وفئات وبدايات خاصين:
root:
groups:
-
tags: []
phrases:
- "The [:prefix] [:name] is a [:class] commissioned in [#1880-1910]"
prefix:
groups:
-
tags:
- ['type', 'military']
phrases:
- "HMS"
-
tags:
- ['type', 'civilian']
phrases:
- "SS"
name:
groups:
-
tags:
- ['type', 'military']
phrases:
- "Invincible"
- "Unstoppable"
- "Indefatigable"
- "Inevitable"
- "Inerrant"
-
tags:
- ['type', 'civilian']
phrases:
- "Dromedary"
- "Mule"
- "Camel"
- "Ox"
class:
groups:
-
tags:
- ['type', 'military']
phrases:
- "cutter"
- "torpedo boat"
- "light cruiser"
- "heavy cruiser"
- "battleship"
-
tags:
- ['type', 'civilian']
phrases:
- "steamship"
- "cargo ship"
- "ferry"
ما تغير الآن هو أنه أصبح لدينا مجموعات من العبارات لكل من السفن المدنية والحربية؛ ونحن نستخدم الآن صفة
tags.
أهم شيء يجب ملاحظته هنا هو أن
tags
هي
لائحة من اللوائح
list of lists.
كل صفة
tag
هي لائحة، مثل
['type', 'military']
كل كلمة هي صفة منفردة وليس بصفتين.
هذا الأمر مهم لأنه يعني أن الصفات تشكل هيكل هرمياً، وهذا الأمر له علاقة بالفلترة كما سنرى لاحقاً.
أول طريقة يتم استخدام الأوصاف فيها هي في
إعادة الدمج
الذي ضبطناه إلى وضع
"مفعّل"
سابقاً.
لاحظ أنه عندما استدعينا
Improv.gen()
فإننا أعطيناه وسيطين؛ الأول
'root'
هو اسم القاعدة التي نريد البدء منها.
الثاني هو مجرد
{}
أي، كائن فارغ جديد. هذا هو
نموذجنا،
كائن يحوي بيانات عن النص الذي نترجمه. كل مرة يختار فيها
Improv
عبارة ويستخدمها فإنه يضيفها إلى النموذج
(بافتراض أن إعادة الدمج مفعّلة).
يمكننا القيام بتغيير بسيط على ملف
الـ JavaScript
بحيث يتم فحص النموذج بعد ذلك.
const model = {};
// ولّد النص المنتج ثم اكتبه في سطر الأوامر
console.log(generator.gen('root', model));
console.log(model);
مثال للمخرج:
The HMS Inevitable is a light cruiser commissioned in 1894
{ tags: [ [ 'type', 'military' ] ] }
سنرى أن الصفة التي استخدمناها سابقاً قد أضيفت إلى كائننا الفارغ. يمكنك أن تولّد نموذجاً باكراً باستخدام وسيلة أخرى ثم تنقله إلى Improv، يمكنك أيضاً حفظ النموذج بعد توليد النص (كمتغيّر) لإعادة استخدامه، كتوليد نصوصٍ أخرى باستخدام الصفات نفسها.
لم نتحدث حتى الآن في هذه المقالة عن أهم ميزة في
Improv:
الفلترة.
أتتذكر
؟filters: [Improv.filters.mismatchFilter()]
ما كان يقوم به ذلك السطر هو ضبط المولد لاستخدام
Improv.filters.mismatchFilter()
كفلتره الوحيد. الفلتر هو مجرد دالة تساعد
Improv
على اختيار المجموعات التي سيتم استخدامها عند الانصياع لقاعدة.
يحوي
Improv.filters
مجموعة من الدالات المثبتة التي تعطي فلترات جاهزة لاستخدامها في
Improv،
ولكن لا يمنعك شيء من كتابة فلتراتك الخاصة.
يحوي كتيّب
Improv
على لائحة من الفلترات المثبتة وعن أمثلة لمواضع استخدامها. لكن من أجل أهداف تعليمية، سوف نلقي نظرة فقط على
*mismatch
filter* فلتر
"عدم المطابقة".
ما يقوم به فلتر "عدم المطابقة" هو البحث عن أوصاف تناقض أوصاف النموذج، واستبعاد المجموعات التي تحوي تلك الأوصاف. إنه أنفع وأبسط فلتر موجود فهو يوقف Improv من مناقضة نفسه بفرض أنه قمنا بوضع الأوصاف في نص ملف الإملاء حتى لا يناقض نفسه.
تعريف "التناقضات" في سياقنا هو:
- النموذج والمجموعة التي تحوي على وصف بنفس العنصر الأول؛ - ولكن دون أن تكون جميع العناصر متطابقةً.
لذا،
'type', 'military'
تتناقض مع
'type', 'civilian'
.
ولكنها تدع التكافئ التام
(أي مع
'type', 'military'
)،
كما تدع الأوصاف المختلفة كلياً
(كمثال،
'propulsion', 'sail'
).
لهذا فإن الأوصاف tags هي لوائح lists؛ يمكن تخيل أن أول عنصر هو فئة، والعناصر التالية تعرّف الفئات الثانوية. والأوصاف تعلن عن نوع الشيء الذي يتم توليد نصّه حتى يتم استبعاد التناقضات؛ لا يمكن لسفينة أن تكون سفينة عسكرية ومدنية، أو كبير وصغيرة بنفس الوقت؛ لكن يمكن للسفينة أن تكون عسكرية وتحوي على أشرعة، أو أن تكون مدنية وكبيرة.
يمكنك إضافة أوصاف وتنويعات أخرى لتوسيع ملف الإملاء هذا، وأن تصنع نصوصاً أكثر تعقيداً؛ يمكنك اسكتشاف البرنامج الإستعراضي الموجود في مجلد Improv على موقع GitHub، والذي يحوي مثالاً حياً بالغ التفصيل لتوليد نصوصٍ عن سفن خيالية عسكرية.
لا يتعمق هذا الدرس في وظائف Improv. أكثرية الفلترات لا تقوم بطرح المجموعات كلياً؛ بل تقوم بإرجاع عدد (موجب أو سالب)، ويتم جمع تلك الأعداد المرجعة من الفلترات كلها لإعطاء كل مجموعة *درجة ملائمة*. يقوم Improv بعد ذلك باختيار ما يجب استخدامه بناءاً على درجة الملائمة. هذه العملية مفيدة لأكثر من إيقاف النص من مناقضة نفسه. يستخدم مولّد نصوص لعبة Voyageur "وصفة سرية" من وسائل الفلترة لتوليد النصوص. أحد الجوانب التي يتم تقديرها هو سعة النص الموصوف. أي محاولة استخدام أوصاف لم يتم استخدامها سابقاً لإنتاج فقرة تحوي جميع جوانب الشيء الموصف. يحاول المولّد أيضاً البحث عن التعيين. أي تثمين كتل النص المتعلقة بظروف محددة بحيث تظهر في الفرص النادرة المناسبة.