Mert Özen Her Satırda Daha İleri
Yapay Zeka LLM Temelleri — #06

LLM'lerde Sampling: Temperature, Top-p, Top-k ve Seed

Mert Özen 6 Kas 2026 12 dk 3 görüntülenme
LLM'lerde Sampling: Temperature, Top-p, Top-k ve Seed

Aynı soruya neden farklı cevaplar geliyor? logprobs ile modelin aday token'larına bakıp temperature, top-k ve top-p'nin bu dağılımı nasıl değiştirdiğini hesaplıyor, seed'in tekrarlanabilirlik sözünü ve sınırlarını, num_predict ve stop sequence'leri gemma3:4b üzerinde gerçek çıktılarla görüyoruz.

Sürüm notu: Bu yazıdaki örnekler Ollama 0.34 ile yerel modellerde (gemma3:4b) denenmiştir; .NET 10 ve Node 22 kullanılmıştır.

Önce Tahmin Et

Modele aynı soruyu, aynı kelimelerle beş kez soruyorsun. Beş kez aynı cevabı mı alırsın? Peki temperature değerini 0'a çekip üstüne bir seed verirsen, cevabın her seferinde aynı olacağına söz verebilir misin?

İlk soruya çoğu geliştirici "bilgisayar sonuçta, tabii ki aynı" diye cevap veriyor. İkinci soruya "kesinlikle" diyen de az değil. Birazdan ikisini de gerçek çalıştırmalarla göreceğiz; ikinci sorunun cevabı beklediğinden daha nüanslı.

Aynı Soru, Farklı Cevaplar

Bookstore asistanımız için yeni bir şube adı arıyoruz. Aşağıdaki TypeScript kodu aynı soruyu önce modelin varsayılan ayarlarıyla, sonra temperature: 0 ile beşer kez soruyor:

import ollama from 'ollama';

const prompt = 'Suggest a name for a new bookstore. Reply with the name only.';

async function ask(options: { temperature?: number; seed?: number } = {}): Promise<string> {
  const res = await ollama.chat({
    model: 'gemma3:4b',
    messages: [{ role: 'user', content: prompt }],
    options,
  });
  return res.message.content.trim();
}

console.log('--- model defaults');
for (let i = 0; i < 5; i++) console.log(await ask());

console.log('--- temperature 0');
for (let i = 0; i < 5; i++) console.log(await ask({ temperature: 0 }));
--- model defaults
The Written Page
The Paper Lantern
The Storyteller's Nook
The Written Page
The Written Page
--- temperature 0
The Book Nook
The Book Nook
The Book Nook
The Book Nook
The Book Nook

Varsayılan ayarlarla beş denemede üç farklı isim geldi. temperature: 0 ile beşi de aynı. İlginç olan şu: sıfır sıcaklıktaki cevap, yani "The Book Nook", ilk beş denemenin hiçbirinde çıkmadı. Model rastgele davrandığında her seferinde "en iyi" cevabı vermiyor, olası cevaplardan birini seçiyor.

Serinin ilk yazısından hatırlarsan model her adımda bir sonraki token için bir olasılık dağılımı hesaplıyor. Bu dağılımdan tek bir token seçme işine sampling (örnekleme) deniyor. Bu yazıdaki bütün ayarlar o seçimin nasıl yapılacağını belirliyor.

Perdenin Arkası: Aday Token'lar

Kullandığımız Ollama sürümü, cevapla birlikte her adımdaki en olası adayları da döndürebiliyor: logprobs: true ve top_logprobs ile. Logprob, olasılığın doğal logaritması; Math.exp ile tekrar yüzdeye çeviriyoruz.

import ollama from 'ollama';

const res = await ollama.chat({
  model: 'gemma3:4b',
  messages: [{ role: 'user', content: 'Suggest a name for a new bookstore. Reply with the name only.' }],
  options: { temperature: 0 },
  logprobs: true,
  top_logprobs: 5,
});

// Üretilen her token için en olası beş adayı göster
for (const step of res.logprobs!.slice(0, 3)) {
  const candidates = step.top_logprobs!
    .map((c) => `${JSON.stringify(c.token)} ${(Math.exp(c.logprob) * 100).toFixed(1)}%`)
    .join(' | ');
  console.log(`${JSON.stringify(step.token).padEnd(8)} <- ${candidates}`);
}
"The"    <- "The" 99.3% | "Ink" 0.6% | "Page" 0.0% | "Pages" 0.0% | "Chapter" 0.0%
" Book"  <- " Book" 37.6% | " Written" 25.8% | " Story" 23.1% | " Ink" 7.4% | " Paper" 2.4%
" N"     <- " N" 100.0% | " Burrow" 0.0% | "binder" 0.0% | "worm" 0.0% | " Nest" 0.0%

Çıktı her şeyi açıklıyor. İlk adımda model neredeyse emin: %99,3 ihtimalle "The". Üçüncü adımda da " N" (Nook'un başı) %100'e yakın. Asıl karar ikinci adımda veriliyor: " Book" %37,6, " Written" %25,8, " Story" %23,1. Varsayılan ayarlarla gelen "The Written Page", "The Storyteller's Nook" ve "The Paper Lantern" tam olarak bu adaylardan doğdu. Model aslında üç dört ismi aşağı yukarı aynı derecede makul buluyor; hangisini söyleyeceği o adımda zarın nasıl geldiğine bağlı.

Bir gözlem daha: aynı isteği farklı temperature değerleriyle çalıştırdığımda bu yüzdeler değişmedi. Yani logprobs modelin ham dağılımını gösteriyor; temperature ve diğer ayarlar bu dağılımın üstüne, seçim anında uygulanıyor.

Temperature, Top-k ve Top-p

Şimdi bu ayarların aynı aday listesine ne yaptığını elle hesaplayalım. Aşağıdaki kod ikinci adımın beş adayını alıyor, önce farklı temperature değerleriyle olasılıkları yeniden hesaplıyor, sonra top-p'nin hangi adayları bıraktığına bakıyor:

import ollama from 'ollama';

const res = await ollama.chat({
  model: 'gemma3:4b',
  messages: [{ role: 'user', content: 'Suggest a name for a new bookstore. Reply with the name only.' }],
  options: { temperature: 0 },
  logprobs: true,
  top_logprobs: 5,
});

// İkinci token: "The" sonrasındaki adaylar
const candidates = res.logprobs![1].top_logprobs!;

function withTemperature(t: number): string {
  const weights = candidates.map((c) => Math.exp(c.logprob / t));
  const sum = weights.reduce((a, b) => a + b, 0);
  return candidates
    .map((c, i) => `${c.token.trim()} ${((weights[i] / sum) * 100).toFixed(0)}%`)
    .join(', ');
}

for (const t of [0.2, 0.7, 1.0, 1.5]) {
  console.log(`T=${t}: ${withTemperature(t)}`);
}

// Top-p: toplam olasılığı p'ye ulaşan en küçük aday kümesini tut
function nucleus(p: number): string[] {
  const kept: string[] = [];
  let total = 0;
  for (const c of candidates) {
    kept.push(c.token.trim());
    total += Math.exp(c.logprob);
    if (total >= p) break;
  }
  return kept;
}

console.log('top_p=0.5:', nucleus(0.5));
console.log('top_p=0.9:', nucleus(0.9));
T=0.2: Book 81%, Written 12%, Story 7%, Ink 0%, Paper 0%
T=0.7: Book 45%, Written 27%, Story 23%, Ink 4%, Paper 1%
T=1: Book 39%, Written 27%, Story 24%, Ink 8%, Paper 2%
T=1.5: Book 33%, Written 26%, Story 24%, Ink 11%, Paper 5%
top_p=0.5: [ 'Book', 'Written' ]
top_p=0.9: [ 'Book', 'Written', 'Story', 'Ink' ]

Temperature, softmax'tan önce modelin ham skorlarını (logit) bir sayıya bölmek demek; logprob'u T'ye bölüp yeniden normalize etmek aynı sonucu verir. T küçüldükçe dağılım sivrileşir: 0,2'de " Book" %81'e çıkıyor, diğerleri sıfıra yaklaşıyor. T büyüdükçe dağılım yassılaşır: 1,5'te zayıf adaylar ("Paper" %5) ciddi şans kazanıyor. T sıfıra yaklaştığında her seferinde en olası token seçilir; buna greedy decoding deniyor. Bir uyarı: hesabı yalnızca ilk beş aday üzerinden yaptık, bu yüzden T=1 satırı ham değerlerden birkaç puan farklı. Gerçek sampling bütün sözlük üzerinde çalışır.

Top-k daha kaba bir makas: yalnızca en olası k adayı bırakır, gerisini atar. Top-p (nucleus sampling, Holtzman ve arkadaşlarının 2019 tarihli makalesinden) ise sayıya değil toplam olasılığa bakar: adayları büyükten küçüğe toplar ve toplam p'ye ulaştığı anda durur. Çıktıda top_p=0,5 yalnızca iki adayı bıraktı, 0,9 ise dördünü. Avantajı şu: model eminken (ilk adımdaki %99,3 gibi) havuz tek adaya iner, kararsızken genişler. Top-k ise her durumda aynı sayıda aday bırakır.

Bu değerleri model de varsayılan olarak taşıyor. Ollama'da modelin Modelfile'ındaki parametrelere bakabilirsin:

$ ollama show gemma3:4b --parameters
stop                           "<end_of_turn>"
temperature                    1
top_k                          64
top_p                          0.95
$ ollama show qwen3:4b --parameters
top_k                          20
top_p                          0.95
repeat_penalty                 1
stop                           "<|im_start|>"
stop                           "<|im_end|>"
temperature                    0.6

gemma3 varsayılan olarak temperature 1, top_k 64, top_p 0.95 ile geliyor; qwen3 ise 0,6, 20 ve 0,95 ile. İsteğinde bir değer vermezsen bunlar kullanılıyor. Yani "hiçbir ayar vermedim" demek aslında "modeli paketleyenin seçtiği ayarları kullanıyorum" demek ve bu ayarlar modelden modele değişiyor.

Seed: Rastgeleliği Tekrarlanabilir Yapmak

Bazen çeşitlilik istersin ama aynı çeşitliliği tekrar üretebilmen de gerekir: bir hatayı yeniden oluşturmak, bir testi sabitlemek ya da iki prompt'u adil karşılaştırmak için. Seed, sampling'in kullandığı rastgele sayı üretecinin başlangıç değeri; aynı seed, aynı rastgele sayı sırası demek. C# tarafında Microsoft.Extensions.AI'nin ChatOptions class'ı bunu doğrudan taşıyor:

using Microsoft.Extensions.AI;
using OllamaSharp;

IChatClient client = new OllamaApiClient(new Uri("http://localhost:11434"), "gemma3:4b");
string prompt = "Write a one-sentence tagline for a small bookstore. Reply with the tagline only.";

long[] seeds = [42, 42, 7, 7];
foreach (long seed in seeds)
{
    ChatOptions options = new() { Temperature = 0.8f, Seed = seed };
    ChatResponse response = await client.GetResponseAsync(prompt, options);
    Console.WriteLine($"seed {seed}: {response.Text.Trim()}");
}
seed 42: Lose yourself in the pages, one story at a time.
seed 42: Lose yourself in the pages, one story at a time.
seed 7: Discover your next story at [Bookstore Name].
seed 7: Discover your next story at [Bookstore Name].

Temperature = 0.8f ile rastgelelik açık, ama aynı seed aynı cümleyi verdi, farklı seed başka bir cümleyi. OllamaSharp bu property'leri Ollama'nın options object'ine çeviriyor: Temperature → temperature, TopP → top_p, TopK → top_k, Seed → seed, MaxOutputTokens → num_predict, StopSequences → stop. Bunu, giden isteği bir DelegatingHandler ile yakalayıp kontrol ettim.

Seed'in verdiği söz sınırlı. Microsoft.Extensions.AI dokümanı da Seed'i sonuçların tekrarlanabilirliğini kontrol etmek için sağlayıcının kullandığı bir değer olarak tarif ediyor, bir garanti olarak değil. Aynı sonucu ancak aynı model build'i, aynı quantization, aynı donanım ve backend, aynı seçenekler ve benzer bir yük altında bekleyebilirsin. GPU'daki floating-point hesapları ve isteklerin gruplanması (batching) yine fark yaratabilir. Bunun canlı bir örneğini tuzaklar bölümünde göstereceğim.

Cevabın Boyunu Sınırlamak: num_predict ve Stop Sequence'ler

Sampling yalnızca hangi token'ın seçileceği değil, üretimin ne zaman duracağı da. Normalde model kendi bitiş token'ını ürettiğinde durur. Buna iki kontrol daha ekleyebilirsin:

using Microsoft.Extensions.AI;
using OllamaSharp;

IChatClient client = new OllamaApiClient(new Uri("http://localhost:11434"), "gemma3:4b");
string prompt = "List five classic novels, one per line, numbered 1 to 5. No extra text.";

ChatResponse full = await client.GetResponseAsync(prompt, new ChatOptions { Temperature = 0 });
Print("no limit", full);

ChatResponse cut = await client.GetResponseAsync(prompt, new ChatOptions { Temperature = 0, MaxOutputTokens = 12 });
Print("MaxOutputTokens = 12", cut);

ChatResponse stopped = await client.GetResponseAsync(prompt, new ChatOptions { Temperature = 0, StopSequences = ["4."] });
Print("StopSequences = [\"4.\"]", stopped);

static void Print(string title, ChatResponse response)
{
    Console.WriteLine($"--- {title}");
    Console.WriteLine(response.Text);
    Console.WriteLine($"finish: {response.FinishReason}, output tokens: {response.Usage?.OutputTokenCount}");
}
--- no limit
1. Pride and Prejudice
2. To Kill a Mockingbird
3. 1984
4. Jane Eyre
5. The Great Gatsby

finish: stop, output tokens: 36
--- MaxOutputTokens = 12
1. Pride and Prejudice
2. To Kill a Mock
finish: length, output tokens: 12
--- StopSequences = ["4."]
1. Pride and Prejudice
2. To Kill a Mockingbird
3. 1984

finish: stop, output tokens: 25

MaxOutputTokens (Ollama'da num_predict) üretilecek token sayısına sert bir tavan koyar. Model "To Kill a Mock" diye kelimenin ortasında kesildi ve bitiş sebebi length oldu. Bu sebebi kontrol etmeden cevabı kullanırsan kesik bir metni tamam sanırsın; JSON üretirken bu, parse edilemeyen yarım bir object demek. Structured output yazısında buna tekrar döneceğiz.

StopSequences (Ollama'da stop) ise belirli bir metin üretildiğinde durdurur. "4." görülünce üretim kesildi ve dikkat: durdurucu metin cevaba dahil edilmedi, bitiş sebebi de stop olarak geldi. Yani doğal bir bitişi stop sequence'e takılmaktan yalnızca bu alana bakarak ayıramazsın. Stop sequence'ler cevabın bir kalıbın ötesine geçmesini istemediğin yerlerde işe yarar: tek satırlık cevap için satır sonu, bir template'te bir sonraki etiketin başlangıcı gibi. Modelin chat template'i de aynı mekanizmayı kullanıyor; gemma3'teki stop "<end_of_turn>" tam olarak bu.

Görev Türüne Göre Ayar

Tek bir "doğru" ayar yok; görevin ne istediğine bakmak gerekiyor. Pratikte kullandığım ayrım şu: cevabın tek bir doğrusu varsa (sınıflandırma, bilgi çıkarma, JSON, kod dönüşümü) rastgeleliği kapat. Cevabın birden fazla iyi hâli varsa (kitap tanıtımı, başlık önerisi, slogan) biraz aç. Bunu kodda dağınık sayılar yerine isimli preset'lerle tutmak okunabilirliği artırıyor:

using Microsoft.Extensions.AI;
using OllamaSharp;

IChatClient client = new OllamaApiClient(new Uri("http://localhost:11434"), "gemma3:4b");

ChatResponse genre = await client.GetResponseAsync(
    "Which genre is 'Dune' by Frank Herbert? Reply with one word.",
    SamplingPresets.Extraction);
Console.WriteLine($"extraction: {genre.Text.Trim()}");

ChatResponse blurb = await client.GetResponseAsync(
    "Write a two-sentence shelf note for 'Dune' by Frank Herbert.",
    SamplingPresets.Creative);
Console.WriteLine($"creative: {blurb.Text.Trim()}");

static class SamplingPresets
{
    // Sınıflandırma, bilgi çıkarma, JSON: aynı girdi aynı çıktıyı vermeli
    public static ChatOptions Extraction => new() { Temperature = 0f, Seed = 42, MaxOutputTokens = 50 };

    // Raf notu, slogan, öneri: biraz çeşitlilik iyidir
    public static ChatOptions Creative => new() { Temperature = 0.9f, TopP = 0.95f, TopK = 64, MaxOutputTokens = 120 };
}
extraction: Science Fantasy
creative: Dive into a sprawling epic of politics, ecology, and destiny in Frank Herbert's *Dune*. Prepare to be transported to a desert planet unlike any you've ever imagined!

İki gözlem var. Birincisi, "tek kelimeyle cevap ver" dediğimiz hâlde model "Science Fantasy" diye iki kelime yazdı. Temperature = 0 cevabı tekrarlanabilir yapar, modeli daha itaatkâr yapmaz. Talimata uymayan bir cevap sıfır sıcaklıkta da gelir, üstelik her seferinde aynı biçimde gelir. İkincisi, yaratıcı preset'te bile MaxOutputTokens koyduk: çeşitlilik istemek, uzunluğun kontrolden çıkmasına izin vermek demek değil.

Bir de genel kural: temperature ile top-p'yi aynı anda sert biçimde değiştirme. İkisi de aynı dağılımı şekillendiriyor. Birini değiştirip sonucu ölçmek, ikisini birden oynatıp neyin işe yaradığını bilmemekten iyidir.

Sık Düşülen Tuzaklar

İlk tuzak, seed'i bir garanti sanmak. Yukarıdaki seed örneğini ilk çalıştırdığımda çıktı şuydu:

seed 42: Lose yourself in a good book, right here.
seed 42: Lose yourself in the pages, one story at a time.
seed 7: Discover your next story at [Bookstore Name].
seed 7: Discover your next story at [Bookstore Name].

Aynı seed, aynı sıcaklık, aynı prompt, arka arkaya iki istek; ama ilk iki satır farklı. Sonraki üç çalıştırmada dört satır da yukarıda gösterdiğim gibi tutarlı geldi. Sebebi dışarıdan görünmüyor. O sırada aynı Ollama sunucusunu başka işler de kullanıyordu ve istekler arasında sunucunun iç durumu (önbellek, gruplama) değişebiliyor. Ders şu: seed'i hata ayıklama ve karşılaştırma için kullan, ama "aynı girdi her zaman aynı çıktıyı verir" varsayımına dayanan bir iş mantığı kurma. Testlerde birebir metin karşılaştırması yerine cevabın özelliklerini (biçim, uzunluk, beklenen etiket) kontrol et.

İkincisi, varsayılanların modelden modele değiştiğini unutmak. gemma3'ten qwen3'e geçtiğinde, sen hiçbir ayara dokunmasan bile temperature 1'den 0,6'ya, top_k 64'ten 20'ye iniyor. Sabit kalmasını istediğin ayarları her istekte açıkça gönder.

Üçüncüsü, temperature: 0'ı bir kalite ayarı sanmak. Sıfır sıcaklık en olası token'ı seçer; en olası token her zaman en doğru cevap değildir. "Science Fantasy" örneğinde olduğu gibi talimata uymayan bir cevap da sıfır sıcaklıkta tekrar tekrar gelir. Kaliteyi prompt, model seçimi ve doğrulama belirler; sampling yalnızca dağılımdan nasıl seçileceğini belirler.

Dördüncüsü, MaxOutputTokens koyup bitiş sebebini kontrol etmemek. Sınır maliyeti ve gecikmeyi korur ama length ile biten bir cevap yarımdır. Kullanıcıya gösterecek ya da parse edeceksen önce FinishReason'a bak.

Beşincisi, yüksek temperature'ı bir "yaratıcılık" düğmesi sanmak. 1,5'in üstüne çıktıkça zayıf adaylar şans kazanıyor ve bir noktadan sonra bu yaratıcılık değil, tuhaf kelime seçimleri ve konudan sapma oluyor. Çeşitlilik istiyorsan genelde 0,7 ile 1 arası bir temperature ve makul bir top-p yeterli.

Küçük Bir Deneme

★☆☆ same-question.ts'teki prompt'u "Recommend one fantasy novel. Reply with the title only." ile değiştir ve varsayılan ayarlarla beş kez çalıştır. Kaç farklı cevap geldi? Sonra candidates.ts ile aynı prompt'un ilk token'larına bak ve cevabın hangi adımda ayrıştığını bul.

★★☆ reshape.ts'i ilk adımın, yani res.logprobs![0] adaylarının üzerinde çalıştır. Model %99,3 eminken T=1,5 bile dağılımı ne kadar değiştirebiliyor? Bu, yüksek temperature'ın neden her token'ı aynı ölçüde etkilemediğini gösterir.

★★★ Seed örneğini bir döngüde on kez çalıştır ve aynı seed'in kaç kez aynı cümleyi verdiğini say. Sonra ikinci bir terminalden aynı anda başka istekler gönderirken tekrarla ve iki sonucu karşılaştır.

Bu yazıdaki bütün örneklerde cevabın tamamlanmasını bekledik ve ancak ondan sonra ekrana bir şey yazdık. Bir sonraki yazıda streaming'e geçiyoruz: Ollama'nın cevabı parça parça nasıl gönderdiğini, C#'ta IAsyncEnumerable ve TypeScript'te async iterator ile nasıl okuyacağımızı, akışı yarıda nasıl iptal edeceğimizi ve ilk token'ın ne zaman geldiğini ölçmeyi göreceğiz.