Урок · Початковий рівень · ~40 хв
JavaScript · Ланцюги Маркова · NLP

Генератор тексту на ланцюгах Маркова

Ланцюг Маркова генерує текст, памʼятаючи останні N слів і випадково обираючи, що йде далі — на основі частот, спостережених у навчальному корпусі. Ланцюги порядку 2 дають напрочуд звʼязну тарабарщину; ланцюги порядку 4 часто відтворюють точні речення джерела. Створіть один у 60 рядках JavaScript.

1Токенізація та побудова таблиці n-грам

Розбийте навчальний текст на токени (слова, пунктуацію). Для кожної n-грами (вікна з N послідовних токенів) запишіть, який токен іде за нею:

function tokenize(text) { // Зберігаємо слова, апострофи та пунктуацію як окремі токени return text .toLowerCase() .match(/[a-z']+|[.,!?;:"-]/g) || []; } function buildModel(text, order = 2) { const tokens = tokenize(text); const model = new Map(); // ключ: кортеж із N слів → значення: масив наступних слів for (let i = 0; i < tokens.length - order; i++) { const key = tokens.slice(i, i + order).join(' '); const next = tokens[i + order]; if (!model.has(key)) model.set(key, []); model.get(key).push(next); } return model; } // Приклад використання з уривком Шекспіра const corpus = `To be or not to be that is the question whether tis nobler in the mind to suffer the slings and arrows of outrageous fortune …`; const model = buildModel(corpus, 2); // model.get("to be") → ["or", "that", "or", "or", ...] (частоти закодовані як повторення)
Зберігання частот як повторюваних значень (наприклад, ["the","the","the","a"]) замість мап підрахунку робить семплування тривіально простим: просто оберіть випадковий індекс. Використання памʼяті трохи вище, але незначне для типових корпусів.

2Генерування тексту через семплування

function generate(model, order, maxWords = 100, seed = null) { // Починаємо з випадкового ключа або наданої стартової фрази const keys = [...model.keys()]; let current = seed ?? keys[Math.floor(Math.random() * keys.length)]; const words = current.split(' '); for (let i = 0; i < maxWords; i++) { const candidates = model.get(current); if (!candidates || candidates.length === 0) break; const next = candidates[Math.floor(Math.random() * candidates.length)]; words.push(next); // Зсуваємо вікно на одне слово const window = words.slice(-order); current = window.join(' '); } return words.join(' '); } console.log(generate(model, 2, 80)); // → "to be that is the question whether tis nobler in the mind to // suffer the slings and arrows of outrageous fortune whether ..."

3Семплування з температурою

Семплування за чистою частотою завжди надає перевагу найпоширенішим продовженням. Температура керує різноманітністю: менше за 1 = передбачуваніше, більше за 1 = випадковіше:

function temperatureSample(candidates, temperature = 1.0) { if (temperature === 1.0) { return candidates[Math.floor(Math.random() * candidates.length)]; } // Підраховуємо частоти const freq = new Map(); for (const w of candidates) freq.set(w, (freq.get(w) ?? 0) + 1); // Перетворюємо на розподіл ймовірностей, застосовуємо температуру const words = [...freq.keys()]; let logits = words.map(w => Math.log(freq.get(w) / candidates.length) / temperature); const maxLogit = Math.max(...logits); let probs = logits.map(l => Math.exp(l - maxLogit)); const sum = probs.reduce((a, b) => a + b, 0); probs = probs.map(p => p / sum); // Семплуємо з розподілу let r = Math.random(), cumulative = 0; for (let i = 0; i < words.length; i++) { cumulative += probs[i]; if (r <= cumulative) return words[i]; } return words[words.length - 1]; }

4Стартові токени та межі речень

// Додаємо спеціальний маркер <START> на початку речень function buildModelWithBoundaries(text, order = 2) { const sentences = text.split(/[.!?]+/).map(s => s.trim()).filter(Boolean); const model = new Map(); for (const sentence of sentences) { const tokens = tokenize(sentence); const padded = [...Array(order).fill('<START>'), ...tokens, '<END>']; for (let i = 0; i < padded.length - order; i++) { const key = padded.slice(i, i + order).join(' '); const next = padded[i + order]; if (!model.has(key)) model.set(key, []); model.get(key).push(next); } } return model; } // Генератор: завжди починаємо з <START> і зупиняємось на <END> function generateSentence(model, order, temperature = 1.0) { let current = Array(order).fill('<START>').join(' '); const words = []; for (let i = 0; i < 200; i++) { const candidates = model.get(current); if (!candidates) break; const next = temperatureSample(candidates, temperature); if (next === '<END>') break; words.push(next); current = [...current.split(' ').slice(1), next].join(' '); } return words.join(' '); }

5Побудова інтерфейсу у браузері

<!-- Каркас HTML --> <textarea id="corpus" rows="8" placeholder="Вставте навчальний текст…"></textarea> <label>Порядок: <input id="order" type="range" min="1" max="5" value="2"> <span id="orderVal">2</span></label> <label>Температура: <input id="temp" type="range" min="1" max="20" value="10"> <span id="tempVal">1.0</span></label> <button id="train">Навчити</button> <button id="gen">Згенерувати</button> <div id="output"></div> <script> let model = null, currentOrder = 2; document.getElementById('order').addEventListener('input', e => { document.getElementById('orderVal').textContent = currentOrder = +e.target.value; }); document.getElementById('temp').addEventListener('input', e => { document.getElementById('tempVal').textContent = (+e.target.value / 10).toFixed(1); }); document.getElementById('train').addEventListener('click', () => { const corpus = document.getElementById('corpus').value; model = buildModelWithBoundaries(corpus, currentOrder); document.getElementById('output').textContent = `Навчено. ${model.size} унікальних станів.`; }); document.getElementById('gen').addEventListener('click', () => { if (!model) return; const temp = +document.getElementById('temp').value / 10; const text = Array.from({length: 5}, () => generateSentence(model, currentOrder, temp)).join(' '); document.getElementById('output').textContent = text; }); </script>
Спробуйте порядок 1 (дуже випадковий), порядок 3 (звʼязні абзаци) та порядок 5 (майже запамʼятовує джерело). Золота середина для цікавого згенерованого тексту — зазвичай порядок 2–3 з температурою 0.8–1.2.