Урок · Початковий рівень · ~40 хв
Генератор тексту на ланцюгах Маркова
Ланцюг Маркова генерує текст, памʼятаючи останні 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.