Reinforcement Learning
Rehber Yapay Zeka RL · Temel

RL Temelleri
MDP & Q-Learning.

Agent-environment döngüsünden Bellman denklemine, tabular Q-learning'den TD(λ)'ya. FrozenLake üzerinde sıfırdan convergence'a.

00 Reinforcement Learning Nedir?

Bir agent, bir ortamla etkileşerek ödül sinyallerinden öğrenir — ne yapacağı önceden söylenmeden.

Makine öğrenmesinin üç ana paradigması arasında reinforcement learning (RL), en çok insan öğrenmesine benzer olanıdır. Supervised learning'de etiketli veri verilir; unsupervised learning'de yapı keşfedilir. RL'de ise bir agent, kararlar alır, sonuçları gözlemler ve bu deneyimlerden nasıl daha iyi davranacağını öğrenir.

Temel döngü şudur: agent, ortamın mevcut state'ini (durumunu) gözlemler, bir action (eylem) seçer, ortam yeni bir state'e geçer ve agent bir reward (ödül) sinyali alır. Bu döngü bir episode boyunca tekrarlanır. Satranç oynayan bir program, robot kolu kontrol eden bir sistem, reklam gösterimi optimize eden bir algoritma — hepsi bu çerçeveye girer.

RL'İN TEMEL FARKI

RL'de doğrudan "doğru cevap" yoktur. Agent, gecikmeli ve seyrek ödül sinyallerinden uzun vadeli strateji öğrenir. Bu, credit assignment problem'ini doğurur: hangi eylem bu ödüle yol açtı?

Agent
State'i gözlemler
→ action →
Ortam
State geçişi
→ reward, state' →
Agent
Güncelleme

RL'in tarihsel kökleri 1950'lere, Bellman'ın dinamik programlama çalışmalarına dayanır. 1992'de TD-Gammon tavla oynayıp insan şampiyonlarına meydan okudu. 2013'te DeepMind'ın DQN'i Atari oyunlarını öğrendi. 2016'da AlphaGo dünya şampiyonunu yendi. 2022'de RL tabanlı sistemler robotu üretim hattına taşıdı.

01 Markov Decision Process (MDP)

MDP, RL problemini matematiksel olarak tanımlayan beşli bir tupledır: (S, A, P, R, γ).

Formal olarak bir Markov Decision Process şu bileşenlerden oluşur:

S State space — ortamın tüm olası durumlarının kümesi. Sonlu (FrozenLake'teki 16 kare) veya sonsuz (robot eklemlerinin açı değerleri) olabilir.
A Action space — agent'ın her durumda alabileceği eylemlerin kümesi. Ayrık (sol/sağ/yukarı/aşağı) veya sürekli (tork miktarı) olabilir.
P(s'|s,a) Transition probability — s durumunda a eylemini alınca s' durumuna geçme olasılığı. Stokastik ortamlarda bu dağılım bilgisi yoktur.
R(s,a,s') Reward function — s'den a ile s' geçişinde alınan anlık ödül. Tasarımı kritiktir: yanlış tanımlanmış reward, beklenmedik davranışlara yol açar.
γ Discount factor — [0,1] aralığında. Gelecek ödüllerin bugünkü değerini belirler. γ=0: yalnızca anlık ödül; γ→1: uzun vadeli düşünme.

Markov özelliği: Bir süreç Markov ise gelecek, yalnızca mevcut state'e bağlıdır — geçmişe değil. Formülde: P(s_{t+1}|s_t, a_t) = P(s_{t+1}|s_0,a_0,...,s_t,a_t). Bu varsayım, hesaplamayı büyük ölçüde basitleştirir.

mdp_basics.py
import gymnasium as gym
import numpy as np

# FrozenLake: 4x4 grid, 16 state, 4 action (L/D/R/U)
env = gym.make('FrozenLake-v1', is_slippery=False)

print(f"State space  : {env.observation_space}")   # Discrete(16)
print(f"Action space : {env.action_space}")         # Discrete(4)

# MDP bileşenlerini incele
# P[state][action] = [(prob, next_state, reward, done), ...]
transition = env.unwrapped.P[0][2]   # state=0, action=RIGHT
print(f"Geçiş: {transition}")
# [(1.0, 1, 0.0, False)]  — deterministik

# Episode başlatma ve adım atma
state, info = env.reset(seed=42)
print(f"Başlangıç state: {state}")      # 0 (sol üst köşe)

total_reward = 0
for _ in range(10):
    action = env.action_space.sample()   # rastgele eylem
    next_state, reward, terminated, truncated, info = env.step(action)
    total_reward += reward
    print(f"  s={state} a={action} → s'={next_state} r={reward} done={terminated}")
    state = next_state
    if terminated or truncated:
        break

env.close()

02 Bellman Denklemi & Dynamic Programming

Bellman denklemi, optimal değer fonksiyonunu özyinelemeli olarak tanımlar — RL'in matematiksel temeli.

Richard Bellman, 1957'de optimal kontrol için şu temel özyinelemeyi türetti. State-value function V^π(s): π politikasını izleyerek s state'inden başlangıçta elde edilecek toplam beklenen discounted ödül.

Bellman Beklenti Denklemi:

V^π(s) = Σ_a π(a|s) · Σ_s' P(s'|s,a) · [R(s,a,s') + γ·V^π(s')]

Bellman Optimallik Denklemi:

V*(s) = max_a Σ_s' P(s'|s,a) · [R(s,a,s') + γ·V*(s')]

Dynamic programming bu denklemleri iteratif olarak çözer. Value Iteration algoritması V*(s)'i doğrudan hesaplar; Policy Iteration ise policy evaluation ve policy improvement adımlarını dönüşümlü uygular.

value_iteration.py
import gymnasium as gym
import numpy as np

def value_iteration(env, gamma=0.99, theta=1e-8):
    """
    Bellman optimallik denklemiyle V*(s) hesapla.
    Transition tablosuna erişim gerektirir (model-based).
    """
    n_states  = env.observation_space.n
    n_actions = env.action_space.n
    V = np.zeros(n_states)

    iteration = 0
    while True:
        delta = 0
        for s in range(n_states):
            v_old = V[s]
            action_values = []
            for a in range(n_actions):
                q = 0
                for prob, s_next, reward, done in env.unwrapped.P[s][a]:
                    q += prob * (reward + gamma * V[s_next] * (1 - done))
                action_values.append(q)
            V[s] = max(action_values)
            delta = max(delta, abs(v_old - V[s]))
        iteration += 1
        if delta < theta:
            break

    # Optimal policy çıkar
    policy = np.zeros(n_states, dtype=int)
    for s in range(n_states):
        q_vals = []
        for a in range(n_actions):
            q = sum(prob * (r + gamma * V[s_] * (1-d))
                    for prob, s_, r, d in env.unwrapped.P[s][a])
            q_vals.append(q)
        policy[s] = np.argmax(q_vals)

    print(f"Value iteration {iteration} iterasyonda converge etti.")
    return V, policy

env = gym.make('FrozenLake-v1', is_slippery=False)
V_star, pi_star = value_iteration(env)
print("V*(s) =\n", V_star.reshape(4,4).round(3))
print("π*(s) =\n", pi_star.reshape(4,4))
# 0=L 1=D 2=R 3=U
DP SINIRI

Dynamic programming, tam transition modelini (P tablosunu) gerektirir. Gerçek dünya problemlerinde bu tablo bilinmez veya state/action uzayı çok büyüktür. Model-free yöntemler bu sorunu, deneyimden öğrenerek aşar.

03 Policy ve Value Function

Policy ne yapılacağını, value function ise bulunulan durumun uzun vadeli değerini söyler.

RL'de iki temel nesne birbirini tamamlar:

π(a|s) Policy — s durumundayken a eylemini seçme olasılığı. Deterministik politikada π(s) = a. Stokastik politikada bir olasılık dağılımıdır.
V^π(s) State-value function — s'den başlayarak π'yi izleyerek elde edilecek toplam beklenen ödül: E[Σ γ^t · r_t | s_0=s].
Q^π(s,a) Action-value (Q) function — s'de a eylemini alıp sonra π'yi izleyerek elde edilecek beklenen ödül: E[Σ γ^t · r_t | s_0=s, a_0=a].
A^π(s,a) Advantage function — A(s,a) = Q(s,a) - V(s). Bir eylemin ortalamaya göre ne kadar iyi olduğunu ölçer. Actor-Critic'in çekirdeği.

Aradaki ilişki: V^π(s) = Σ_a π(a|s) · Q^π(s,a). Optimal policy, her durumda en yüksek Q değerine sahip eylemi seçer: π*(s) = argmax_a Q*(s,a).

NEDEN Q FUNCTION?

V(s) sadece state'in değerini verir; hangi eylemi alacağımızı söylemez. Q(s,a) ise doğrudan eylem seçimine olanak tanır: a* = argmax_a Q(s,a). Bu nedenle model-free algoritmalar genellikle Q fonksiyonunu öğrenir.

04 Tabular Q-Learning

Q-learning, model bilmeksizin Q*(s,a)'yı öğrenen, off-policy bir temporal difference algoritmasıdır.

Watkins ve Dayan'ın 1992 tarihli kanıtı, belirli koşullar altında Q-learning'in optimal Q fonksiyonuna yakınsadığını gösterir. Güncelleme kuralı:

Q(s,a) ← Q(s,a) + α · [r + γ · max_{a'} Q(s',a') - Q(s,a)]

Köşeli parantez içindeki ifadeye TD error (δ) denir. α öğrenme hızıdır. Algoritma off-policy'dir: davranış politikası (epsilon-greedy) ile güncelleme hedefi (greedy max) farklıdır.

Convergence koşulları: (1) Her (s,a) çifti sonsuz kez ziyaret edilmeli. (2) Öğrenme hızı α_t koşullarını sağlamalı: Σ α_t = ∞ ve Σ α_t² < ∞ (örn. α_t = 1/t). (3) Bounded rewards.

q_learning.py
import gymnasium as gym
import numpy as np

def q_learning(env_name='FrozenLake-v1',
               n_episodes=5000,
               alpha=0.1,
               gamma=0.99,
               eps_start=1.0,
               eps_end=0.01,
               eps_decay=0.995):

    env = gym.make(env_name, is_slippery=False)
    n_s = env.observation_space.n
    n_a = env.action_space.n
    Q   = np.zeros((n_s, n_a))     # Q-tablo başlangıcı

    eps      = eps_start
    rewards  = []

    for ep in range(n_episodes):
        state, _ = env.reset()
        total_r   = 0

        while True:
            # Epsilon-greedy eylem seçimi
            if np.random.random() < eps:
                action = env.action_space.sample()   # keşif
            else:
                action = np.argmax(Q[state])          # sömürü

            next_state, reward, terminated, truncated, _ = env.step(action)

            # Q-learning güncellemesi
            td_target = reward + gamma * np.max(Q[next_state]) * (1 - terminated)
            td_error  = td_target - Q[state, action]
            Q[state, action] += alpha * td_error

            total_r += reward
            state    = next_state

            if terminated or truncated:
                break

        eps = max(eps_end, eps * eps_decay)
        rewards.append(total_r)

        if (ep + 1) % 500 == 0:
            win_rate = np.mean(rewards[-500:])
            print(f"Ep {ep+1:5d} | Win rate: {win_rate:.3f} | eps: {eps:.3f}")

    env.close()
    return Q, rewards

Q, rewards = q_learning()
print("\nÖğrenilen Q-tablo:")
print(Q.round(2))

05 Epsilon-Greedy Keşif

Exploration-exploitation dengesi RL'in en temel sorunlarından biridir — mevcut bilgiyi kullanmak mı, yoksa yeni bilgi toplamak mı?

Eğer agent her zaman mevcut en iyi eylemi seçerse (tam greedy), lokal optimuma sıkışabilir. Hiç greedy davranmazsa (tam random) öğrenme olmaz. Epsilon-greedy bu dengeyi ε parametresiyle kurar: ε olasılıkla rastgele, 1-ε olasılıkla greedy eylem seç.

Pratikte ε, eğitim boyunca azaltılır (epsilon decay): başlangıçta çok keşif yapılır, sonra bilgi birikince sömürü artar. Yaygın decay stratejileri:

Linear ε = max(ε_min, ε_start - step · decay_rate) — her adımda sabit miktarda düşer.
Exponential ε = max(ε_min, ε · decay) — her episode sonunda çarpımsal azalma. Genellikle 0.995–0.999 arası.
UCB Upper Confidence Bound — Q(s,a) + c · √(ln(N_s) / N(s,a)). Az ziyaret edilen eylemlere otomatik bonus verir.
exploration.py
import numpy as np
import matplotlib.pyplot as plt

def epsilon_schedule(n_steps=10_000, eps_start=1.0, eps_end=0.05):
    """Farklı decay stratejilerini karşılaştır."""
    steps = np.arange(n_steps)

    # 1. Exponential decay
    decay = (eps_end / eps_start) ** (1 / n_steps)
    eps_exp = eps_start * (decay ** steps)
    eps_exp = np.maximum(eps_exp, eps_end)

    # 2. Linear decay
    eps_lin = np.linspace(eps_start, eps_end, n_steps)

    # 3. Cosine annealing
    eps_cos = eps_end + 0.5 * (eps_start - eps_end) * (
        1 + np.cos(np.pi * steps / n_steps))

    plt.figure(figsize=(10, 4))
    plt.plot(steps, eps_exp, label='Exponential', linewidth=2)
    plt.plot(steps, eps_lin, label='Linear',      linewidth=2)
    plt.plot(steps, eps_cos, label='Cosine',      linewidth=2)
    plt.xlabel('Adım'); plt.ylabel('ε')
    plt.title('Epsilon Decay Stratejileri')
    plt.legend(); plt.tight_layout(); plt.show()

epsilon_schedule()

06 Temporal Difference Learning

TD learning, Monte Carlo ve DP yöntemlerini birleştirir: model gerektirmez, episode bitmeden öğrenir.

Monte Carlo (MC) yöntemleri, bir episode tamamlandıktan sonra gerçek toplam ödülü kullanarak güncelleme yapar. Bu, yüksek variance'a ama sıfır bias'a sahiptir. Dynamic programming ise bootstrapping yapar — bir sonraki state'in tahminine dayanır. Temporal Difference her iki yaklaşımı birleştirir: bir adım sonrasının tahminini (bootstrap) kullanır ama gerçek deneyimden öğrenir.

TD(0) güncellemesi: V(s_t) ← V(s_t) + α · [r_{t+1} + γ·V(s_{t+1}) - V(s_t)]

td_comparison.py
import gymnasium as gym
import numpy as np

def td0_prediction(env, policy, n_episodes=2000, alpha=0.05, gamma=0.99):
    """TD(0) ile policy evaluation — V^π tahmini."""
    n_s = env.observation_space.n
    V   = np.zeros(n_s)

    for _ in range(n_episodes):
        s, _ = env.reset()
        while True:
            a = policy[s]
            s_next, r, done, trunc, _ = env.step(a)
            # TD(0) güncellemesi
            V[s] += alpha * (r + gamma * V[s_next] * (1 - done) - V[s])
            s = s_next
            if done or trunc:
                break
    return V

def mc_prediction(env, policy, n_episodes=2000, gamma=0.99):
    """Monte Carlo ile policy evaluation — tam episode gerektirir."""
    n_s = env.observation_space.n
    V       = np.zeros(n_s)
    returns = [[] for _ in range(n_s)]

    for _ in range(n_episodes):
        traj = []
        s, _ = env.reset()
        while True:
            a = policy[s]
            s_next, r, done, trunc, _ = env.step(a)
            traj.append((s, r))
            s = s_next
            if done or trunc:
                break
        # Geriye doğru return hesapla
        G = 0
        for s_t, r_t in reversed(traj):
            G = r_t + gamma * G
            returns[s_t].append(G)
            V[s_t] = np.mean(returns[s_t])
    return V
TD vs MC KARŞILAŞTIRMA

TD yöntemleri online öğrenir (her adımda güncelleme) ve daha düşük variance'a sahiptir ama bootstrap kaynaklı bias taşır. MC yöntemleri tam episode gerektirir, unbiased tahmin verir ama yüksek variance'lıdır. n-step TD, ikisi arasında bir spektrum oluşturur.

07 Eligibility Traces & TD(λ)

Eligibility traces, geçmişte ziyaret edilen state-action çiftlerine gecikmeli kredi dağıtır — TD ve MC arasındaki köprü.

n-step TD, t anındaki ödülden t+n anına kadarki ödülleri kullanarak bootstrap yapar. TD(λ) ise tüm n-step dönüşlerin geometrik ağırlıklı ortalamasını alır; burada λ ∈ [0,1] bir karışım parametresidir:

G_t^λ = (1-λ) · Σ_{n=1}^∞ λ^{n-1} · G_t^{(n)}

λ=0 → TD(0); λ=1 → Monte Carlo. Eligibility trace e(s), state'in ne kadar yakın ziyaret edildiğini ve ne kadar sorumlu olduğunu takip eder:

e_t(s) = γ·λ·e_{t-1}(s) + 1[s_t = s]

V(s) ← V(s) + α·δ_t·e_t(s) ∀s

td_lambda.py
import gymnasium as gym
import numpy as np

def td_lambda(env, policy, lam=0.9, alpha=0.05, gamma=0.99, n_episodes=2000):
    """TD(λ) accumulating traces ile V^π tahmini."""
    n_s = env.observation_space.n
    V = np.zeros(n_s)

    for _ in range(n_episodes):
        e = np.zeros(n_s)     # eligibility traces sıfırla
        s, _ = env.reset()

        while True:
            a = policy[s]
            s_next, r, done, trunc, _ = env.step(a)

            # TD error
            delta = r + gamma * V[s_next] * (1 - done) - V[s]

            # Trace güncelleme
            e *= gamma * lam
            e[s] += 1                  # accumulating trace

            # Tüm state'leri güncelle
            V += alpha * delta * e

            s = s_next
            if done or trunc:
                break

    return V

env = gym.make('FrozenLake-v1', is_slippery=False)
# Basit deterministik policy: sağ-aşağı stratejisi
policy = np.array([2,2,1,0, 1,0,1,0, 2,1,1,0, 0,2,2,0])
V = td_lambda(env, policy, lam=0.9)
print("V^π (TD-λ=0.9):\n", V.reshape(4,4).round(3))

08 Python ile CartPole Q-Tablosu

CartPole sürekli state uzayına sahip — Q-tablo kullanmak için durumları ayrıklaştırmak gerekir.

CartPole-v1'de state 4 sürekli değerden oluşur: cart position, cart velocity, pole angle, pole angular velocity. Tabular Q-learning için bu sürekli uzayı discretize etmek gerekir. Her boyutu belirli sayıda bin'e böleriz.

cartpole_qtable.py
import gymnasium as gym
import numpy as np
import matplotlib.pyplot as plt

# State ayrıklaştırma parametreleri
N_BINS    = [6, 12, 6, 12]    # her boyut için bin sayısı
STATE_LOW  = np.array([-2.4, -3.0, -0.21, -3.0])
STATE_HIGH = np.array([ 2.4,  3.0,  0.21,  3.0])

BINS = [np.linspace(STATE_LOW[i], STATE_HIGH[i], N_BINS[i] - 1)
        for i in range(4)]

def discretize(obs):
    idx = tuple(np.digitize(obs[i], BINS[i]) for i in range(4))
    return idx

# Q-tablo: N_BINS x 2 actions
Q = np.zeros(N_BINS + [2])

env       = gym.make('CartPole-v1')
alpha     = 0.1
gamma     = 0.99
eps       = 1.0
eps_decay = 0.995
rewards   = []

for ep in range(3000):
    obs, _ = env.reset()
    state  = discretize(obs)
    total  = 0

    while True:
        if np.random.random() < eps:
            action = env.action_space.sample()
        else:
            action = np.argmax(Q[state])

        obs2, r, done, trunc, _ = env.step(action)
        state2 = discretize(obs2)

        best_next = np.max(Q[state2])
        Q[state + (action,)] += alpha * (r + gamma * best_next * (1-done) - Q[state + (action,)])

        state = state2
        total += r
        if done or trunc:
            break

    eps = max(0.01, eps * eps_decay)
    rewards.append(total)

# Smoothed learning curve
window = 100
avg = np.convolve(rewards, np.ones(window)/window, mode='valid')
plt.plot(avg); plt.xlabel('Episode')
plt.ylabel('Ortalama Ödül (100-ep)')
plt.title('CartPole Q-Tablo Learning Curve')
plt.axhline(475, color='r', linestyle='--', label='Çözüm eşiği')
plt.legend(); plt.tight_layout(); plt.show()
print(f"Son 100 ep ort: {np.mean(rewards[-100:]):.1f}")

09 Pratik: FrozenLake Sıfırdan Çözüm

Tüm kavramları birleştiren tam bir Q-learning pipeline — convergence grafiği ve policy visualizasyonuyla.

frozenlake_solve.py
import gymnasium as gym
import numpy as np
import matplotlib.pyplot as plt

def solve_frozenlake(
        is_slippery: bool = True,
        n_episodes: int  = 20_000,
        alpha:      float = 0.1,
        gamma:      float = 0.99,
        eps_start:  float = 1.0,
        eps_end:    float = 0.01,
        eps_decay:  float = 0.9995):

    env = gym.make('FrozenLake-v1', is_slippery=is_slippery)
    Q   = np.zeros((env.observation_space.n, env.action_space.n))
    eps = eps_start
    win_history, eps_history = [], []

    for ep in range(n_episodes):
        s, _ = env.reset()
        won = False

        while True:
            a = (env.action_space.sample() if np.random.random() < eps
                 else np.argmax(Q[s]))
            s2, r, done, trunc, _ = env.step(a)
            Q[s, a] += alpha * (r + gamma * np.max(Q[s2]) * (1-done) - Q[s, a])
            if done and r == 1:
                won = True
            s = s2
            if done or trunc:
                break

        eps = max(eps_end, eps * eps_decay)
        win_history.append(int(won))
        eps_history.append(eps)

    # ── Görselleştirme ──────────────────────────────────────
    window = 500
    win_rate = np.convolve(win_history,
                            np.ones(window)/window, mode='valid')

    fig, axes = plt.subplots(1, 3, figsize=(15, 4))

    # 1. Win rate curve
    axes[0].plot(win_rate, color='steelblue')
    axes[0].axhline(0.78, color='orange', linestyle='--', label='Çözüm eşiği')
    axes[0].set_title('Win Rate'); axes[0].legend()

    # 2. Epsilon curve
    axes[1].plot(eps_history, color='coral')
    axes[1].set_title('Epsilon Decay')

    # 3. Optimal policy heatmap
    policy = np.argmax(Q, axis=1).reshape(4, 4)
    arrows = {0:'←', 1:'↓', 2:'→', 3:'↑'}
    im = axes[2].imshow(np.max(Q, axis=1).reshape(4,4), cmap='YlGnBu')
    for r in range(4):
        for c in range(4):
            axes[2].text(c, r, arrows[policy[r, c]],
                         ha='center', va='center', fontsize=18)
    axes[2].set_title('Optimal Policy')
    fig.colorbar(im, ax=axes[2])

    plt.tight_layout(); plt.show()
    print(f"Son 1000 ep win rate: {np.mean(win_history[-1000:]):.3f}")
    return Q

# Slippery (stokastik) ortam daha zor — daha fazla episode gerekir
Q = solve_frozenlake(is_slippery=True, n_episodes=20_000)
CONVERGENCE NOTU

Slippery FrozenLake'de optimal politika ~%74 win rate üretir (ortam stokastik olduğundan %100 mümkün değildir). Deterministik versiyonda Q-learning kolaylıkla %100 ulaşır. 20.000 episode sonunda ε ≈ 0.01 seviyesine iner ve Q-tablo kararlı hale gelir.