<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged optimisation</title>
<link>https://coderlegion.com/tag/optimisation</link>
<description>Powered by Question2Answer</description>
<item>
<title>The Engineering Behind ShiftSync: Turning Workforce Rostering Into a Constraint-Optimisation Problem</title>
<link>https://coderlegion.com/26044/engineering-behind-shiftsync-turning-workforce-rostering-constraint-optimisation-problem</link>
<description>&lt;p&gt;&lt;em&gt;How 20+ scheduling algorithms, a privacy-first local architecture, and zero mandatory registration combine to make free AI-powered shift scheduling actually work&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Workforce scheduling looks like a UI problem until you try to automate it correctly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Then the real engineering begins.&lt;/strong&gt;&lt;br&gt;
A calendar is trivial to draw. An accurate, fair, legally compliant roster for a 60-person hospital ward covering a 28-day planning period — that is a non-trivial constraint-satisfaction problem that has been forcing managers to stay late with spreadsheets for decades.&lt;br&gt;
ShiftSync is my attempt to solve this properly. It is a browser-based, offline-capable, privacy-first workforce scheduling application with 20+ mathematical algorithms, full leave management, fatigue enforcement, and PDF/Excel export — free, no registration required.&lt;br&gt;
Source code: github.com/validivar/ShiftSync Live application: www.shiftsync.world&lt;br&gt;
This article is about the engineering decisions that made it work — and the ones I had to revisit when my first approach turned out to be wrong.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Start With Constraints, Not the Interface&lt;/strong&gt;&lt;br&gt;
The most seductive mistake in scheduling software is starting with the UI.&lt;br&gt;
You build the staff table. You build the weekly grid. You add colour-coded shift indicators. It looks impressive. Then someone asks: how does it automatically generate a valid schedule?&lt;br&gt;
That is when you discover you have been building the wrong thing first.&lt;br&gt;
Before any interface code, a scheduling system requires a precise constraint model. ShiftSync's constraint model distinguishes between two fundamentally different types of rules:&lt;br&gt;
Hard Constraints — Inviolable&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;An employee on approved leave cannot be assigned to any shift &lt;br&gt;
during their leave period.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No employee may exceed their defined maximum consecutive shift count.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A minimum rest interval (default: 11 hours, EU Working Time &lt;br&gt;
Directive aligned) must separate the end of one shift &lt;br&gt;
and the start of the next.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Any shift designated as requiring a specific skill (ICU, ACLS, &lt;br&gt;
Forklift, etc.) must receive an employee who holds that &lt;br&gt;
certification.&lt;br&gt;
Soft Constraints — Optimised, Not Mandated&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Employees' stated shift preferences should be honoured when &lt;br&gt;
coverage allows.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Total working hours across the planning period should be &lt;br&gt;
distributed equitably across the team.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Night shift assignments should rotate across the full team &lt;br&gt;
rather than concentrating on a subset.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Individual weekly hours should approach (not necessarily &lt;br&gt;
reach) the configured minimum.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The distinction is load-bearing. An algorithm that treats all constraints identically either refuses to generate a roster (too many collisions between hard rules) or generates one that is technically valid but practically awful (maximises compliance metrics while ignoring everything a human manager actually cares about).&lt;br&gt;
The correct model is validity plus optimisation — not just assignment.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why 20 Algorithms Instead of One&lt;/strong&gt;&lt;br&gt;
My first instinct was to build a single generalised scheduling algorithm that could handle any pattern.&lt;br&gt;
This was wrong.&lt;br&gt;
Different industries use shift patterns with fundamentally incompatible structures. The Panama Plan's 2-2-3 cycle is not a generalisation of the Dupont rotation's 28-day sequence. A 24-hour call duty schedule has completely different rest constraint arithmetic than a standard 4x4 eight-hour pattern.&lt;br&gt;
Trying to represent all of these inside a single algorithm produces either a combinatorial explosion of special cases or a lowest-common-denominator system that handles none of them well.&lt;br&gt;
The correct approach is a dispatch table — separate, cleanly implemented algorithms for each pattern, each with full knowledge of its own cycle structure and timing requirements.&lt;br&gt;
const algorithms = {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;'4x4':   generate4x4Schedule,    // 8hr 3-shift rotation
'panama': generatePanamaSchedule, // 12hr 2-2-3 cycle
'pitman': generatePitmanSchedule, // 12hr 4-on-4-off
'dupont': generateDupontSchedule, // 12hr 28-day complex rotation
'ddnnoo': generateDDNNOOSchedule, // 12hr D-D-N-N-O-O cycle
'4-10':  generate4_10Schedule,   // 10hr 4-day week
'8hour': generate8HourSchedule,  // 8hr standard 5/2
'12hour':generate12HourSchedule, // 12hr 3-on-3-off
'24hour':generate24HourSchedule, // 24hr call duty
'48hour':generate48HourSchedule, // 48hr weekend call
'mixed': generateMixedSchedule   // hybrid 8hr+12hr extended nights
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;};&lt;/p&gt;
&lt;p&gt;const fn = algorithms[config.shiftType] || generateFairSchedule;&lt;br&gt;
return fn(startDate, period, staffMembers.length, config);&lt;br&gt;
Each function receives: start date (Luxon DateTime), planning period in days, staff count, and the full config object. Each independently implements its cycle logic before calling checkStaffLeave on every staff-date combination to enforce leave constraints.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Deep Dive: The Dupont Algorithm&lt;/strong&gt;&lt;br&gt;
The Dupont is the most complex pattern to implement correctly because its cycle is 28 days long and mixes night shifts, day shifts, and rest in a non-obvious sequence.&lt;br&gt;
The canonical Dupont sequence is:&lt;br&gt;
N N N N O O O D D D O N N N O O O D D D D O O O O O O O&lt;br&gt;
Where N = Night, D = Day, O = Off.&lt;br&gt;
The critical implementation challenge is assigning teams to positions in this cycle such that:&lt;br&gt;
1.All shifts are covered on all days&lt;br&gt;
2.No team is assigned the same position in consecutive cycles&lt;br&gt;
3.Employee preferences shift the assignment within the cycle without breaking coverage&lt;br&gt;
function generateDupontSchedule(startDate, days, staffCount, config) {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const schedule = [];
const dp = [
    'N','N','N','N','O','O','O',  // 4 nights, 3 off
    'M','M','M','O',               // 3 days, 1 off
    'N','N','N','O','O','O',       // 3 nights, 3 off
    'M','M','M','M',               // 4 days
    'O','O','O','O','O','O','O'   // 7 off
];

for (let day = 0; day &amp;lt; days; day++) {
    const currentDate = startDate.plus({ days: day });
    const dayEntry = { 
        date: currentDate.toFormat('yyyy-MM-dd'),
        dayOfWeek: currentDate.toFormat('EEE'),
        formattedDate: currentDate.toFormat('MMM dd'),
        shifts: {}
    };
    
    staffMembers.forEach((staff, i) =&amp;gt; {
        // Offset each staff member's position in the cycle
        // to ensure continuous coverage
        const cyclePos = (day + Math.floor(i * dp.length / staffCount)) 
                         % dp.length;
        let shift = dp[cyclePos];
        
        // Hard constraint: leave always wins
        const leave = checkStaffLeave(staff, currentDate);
        if (leave) shift = 'L-' + leave;
        
        dayEntry.shifts[staff.id] = { 
            type: shift, 
            staffName: staff.fullName, 
            staffRank: staff.rank, 
            hours: shift === 'O' ? 0 : 12 
        };
    });
    
    schedule.push(dayEntry);
}
return schedule;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;br&gt;
The Math.floor(i * dp.length / staffCount) offset distributes team members across the cycle such that all 28 cycle positions have coverage on every day of the planning period.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Objective Function&lt;/strong&gt;&lt;br&gt;
Two schedules can both satisfy every hard constraint. Which one is better?&lt;br&gt;
ShiftSync's compliance scoring function evaluates quality across four dimensions:&lt;br&gt;
function calculateRosterStats(schedule) {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const stats = {
    totalHours: 0,
    shiftCount: { M:0, A:0, N:0, O:0, C:0 },
    staffHours: {},
    minHours: Infinity,
    maxHours: 0,
    avgHours: 0,
    compliant: true
};

// Accumulate per-staff hours
schedule.forEach(day =&amp;gt; {
    staffMembers.forEach(staff =&amp;gt; {
        const s = day.shifts[staff.id];
        if (s) {
            const t = s.type.charAt(0);
            if (stats.shiftCount[t] !== undefined) stats.shiftCount[t]++;
            stats.totalHours += s.hours || 0;
            stats.staffHours[staff.id] += s.hours || 0;
        }
    });
});

// Evaluate compliance bounds
const arr = Object.values(stats.staffHours);
stats.minHours = Math.min(...arr);
stats.maxHours = Math.max(...arr);
stats.avgHours = arr.reduce((a,b) =&amp;gt; a+b, 0) / arr.length;

const wks = schedule.length / 7;
const maxW = parseInt(document.getElementById('maxHoursWeek').value) || 48;
const minW = parseInt(document.getElementById('minHoursWeek').value) || 36;

stats.compliant = stats.maxHours &amp;lt;= maxW * wks 
               &amp;amp;&amp;amp; stats.minHours &amp;gt;= minW * wks * 0.5;

return stats;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;br&gt;
The compliance flag (PASS / REVIEW) surfaces in the roster display panel. It is not a gate — the manager receives the roster regardless — but the flag ensures that borderline cases are visible rather than buried.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Local-First Architecture: A Deliberate Privacy Stance&lt;/strong&gt;&lt;br&gt;
Workforce scheduling data is operationally sensitive. It reveals staffing levels, employee identities, capability distributions, and leave patterns — information that many organisations in healthcare, security, and government are legitimately concerned about transmitting to third-party cloud infrastructure.&lt;br&gt;
ShiftSync's response to this is architectural, not policy-based. The application has no server-side component. There is no API endpoint receiving scheduling data. There is no cloud database. There is no authentication layer that could become a breach surface.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Traditional SaaS&lt;/strong&gt;:  Browser → API → Database → Application → Browser&lt;br&gt;
ShiftSync:         Browser ← localStorage → Browser&lt;br&gt;
All data is stored in localStorage. All scheduling computation happens in the browser. All export (PDF, Excel) is generated client-side by jsPDF and SheetJS respectively. The application works entirely offline after the initial HTML page load.&lt;br&gt;
This architecture has real limitations for large enterprise deployments that require multi-user collaboration, centralised audit logs, and cross-device synchronisation. Those are genuine trade-offs, not ignored problems.&lt;br&gt;
But for the target audience — small to medium organisations, NGOs, community hospitals, SMEs — this architecture is not a limitation. It is an advantage. It eliminates an entire category of data management risk.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Fatigue Management System&lt;/strong&gt;&lt;br&gt;
EU Working Time Directive requires a minimum 11-hour rest period between shifts. Many clinical governance frameworks require maximum consecutive shift limits. ShiftSync enforces both.&lt;br&gt;
The consecutive shift counter runs during algorithm execution. After generation, the conflict detection system evaluates whether the staff count is sufficient to satisfy the configured limits:&lt;br&gt;
function checkConflicts() {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const maxC = parseInt(document.getElementById('maxConsecutive').value) || 5;
const alertEl = document.getElementById('conflictAlert');

if (staffMembers.length &amp;gt; 0 &amp;amp;&amp;amp; staffMembers.length &amp;lt; maxC) {
    alertEl.classList.add('show');
    document.getElementById('conflictMsg').textContent = 
        `Warning: Only ${staffMembers.length} staff for patterns 
         requiring ${maxC}+ members. Some may exceed consecutive 
         shift limits.`;
} else {
    alertEl.classList.remove('show');
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;br&gt;
The warning surfaces before generation, giving the manager the option to add staff or adjust the consecutive limit before committing to a roster that will need manual correction.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stack Decisions and Why&lt;/strong&gt;&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=15378312219060673527&quot; alt=&quot;&quot;&gt;&lt;br&gt;
The absence of a framework is a deliberate choice. A scheduling tool used by a ward manager in Lagos, a factory supervisor in Manila, and an NGO coordinator in Nairobi needs to load fast on 3G and work when the connection drops. A React bundle adds seconds to the load time and introduces a dependency on JavaScript delivery infrastructure.&lt;br&gt;
Single-file HTML with CDN dependencies loads in under two seconds on a mobile connection. It works offline. It deploys by dragging a file into a hosting panel.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What the Open Source Invitation Is Really For&lt;/strong&gt;&lt;br&gt;
ShiftSync is on GitHub not primarily as a portfolio item, but because scheduling logic is domain-specific in ways that a solo developer building from first principles will get wrong.&lt;br&gt;
The DDNNOO algorithm is straightforward. But is the implementation correct for the specific variant used in ICU environments where one of the two &quot;day&quot; slots has different skill requirements from the other? I do not know. A developer who has worked scheduling in that environment might.&lt;br&gt;
The Panama Plan cycle is mathematically clear. Does it interact correctly with a 14-day planning period where the cycle straddles the boundary? The tests should tell us. They might not cover all cases.&lt;br&gt;
Open source is the mechanism for closing those gaps. If you work in scheduling, healthcare technology, workforce management, or HR software — the codebase is readable, the algorithms are individually separated, and issues are open.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Build It. Try It. Improve It.&lt;/strong&gt;&lt;br&gt;
ShiftSync is live at www.shiftsync.world.&lt;br&gt;
No signup. No email. No credit card. Open the page. Add staff. Select a pattern. Generate. Export.&lt;br&gt;
If you find a constraint the algorithm mishandles, open an issue: github.com/validivar/ShiftSync&lt;br&gt;
If you use it in a production scheduling environment and find something wrong, tell me. That feedback is the most valuable contribution the project can receive.&lt;br&gt;
And if you know an organisation — a clinic, an NGO, a small manufacturer, a security firm — still building rosters in Excel at midnight, send them the link. The better schedule is the product. Help us build it. &lt;br&gt;
The immediate goal to sustaining ShiftSync is maintaining its website. You could help towards website renewal subscription using the donation buttons on the live site.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;🌐 Live app: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.shiftsync.world&quot;&gt;www.shiftsync.world&lt;/a&gt; | ⭐ GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/validivar/ShiftSync&quot;&gt;github.com/validivar/ShiftSync&lt;/a&gt; | &lt;br&gt;
Mikhail Ikpoma — Founder, ShiftSync &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26044/engineering-behind-shiftsync-turning-workforce-rostering-constraint-optimisation-problem</guid>
<pubDate>Wed, 02 Sep 2026 01:40:09 +0000</pubDate>
</item>
<item>
<title>The Cross-Entropy Method: Solving RL Without Gradients</title>
<link>https://coderlegion.com/24679/the-cross-entropy-method-solving-rl-without-gradients</link>
<description>&lt;p&gt;Reinforcement learning has accumulated layers of complexity over the years: value functions, policy gradients, replay buffers, target networks. The Cross-Entropy Method predates all of it. Rubinstein introduced it in 1997 for rare-event simulation, and it turned out to solve simple control tasks with almost no machinery. The entire implementation fits in 50 lines. No gradients, no training loops. Just: sample some parameters, test them, keep the best ones, repeat.&lt;/p&gt;
&lt;p&gt;The Cross-Entropy Method (CEM) is the algorithm you reach for when you want results without complexity. It treats the policy's parameters as a black box, maintains a probability distribution over them, and iteratively narrows that distribution toward high-performing regions. No gradients required. By the end of this post, you'll implement CEM from scratch, solve CartPole-v1 with a perfect score, and understand why this &quot;naive&quot; approach works so well on problems with manageable parameter spaces.&lt;/p&gt;
&lt;h3 id=&quot;let-s-build-it&quot;&gt;Let's Build It&lt;/h3&gt;
&lt;p&gt;Click the badge to open the interactive notebook:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://colab.research.google.com/github/zhubarb/sesen_ai_ml_tutorials/blob/main/notebooks/reinforcement-learning/cross_entropy_method.ipynb&quot;&gt;&lt;img src=&quot;https://colab.research.google.com/assets/colab-badge.svg&quot; alt=&quot;Open In Colab&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://sesen.ai/images/posts/cross-entropy-method-evolution-style-rl-convergence.gif&quot; alt=&quot;CEM convergence animation showing the reward distribution shifting from low to high over 50 iterations&quot;&gt;&lt;/p&gt;
&lt;p&gt;Here's the complete implementation. We use a linear policy with just 4 parameters (one per observation dimension), and CEM finds the perfect weights:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import numpy as np
import gymnasium as gym

def evaluate_policy(env_name, theta):
    &quot;&quot;&quot;Run one episode with a linear policy: action = 1 if theta @ obs &amp;gt; 0 else 0.&quot;&quot;&quot;
    env = gym.make(env_name)
    obs, _ = env.reset()
    total_reward = 0
    done = False
    while not done:
        action = 1 if np.dot(theta, obs) &amp;gt; 0 else 0
        obs, reward, terminated, truncated, _ = env.step(action)
        total_reward += reward
        done = terminated or truncated
    env.close()
    return total_reward

def cem(env_name, n_params, batch_size=200, n_iter=50, elite_frac=0.2,
        initial_std=1.0, extra_std=0.5, std_decay_time=25):
    &quot;&quot;&quot;Cross-Entropy Method for policy search.&quot;&quot;&quot;
    n_elite = int(np.round(batch_size * elite_frac))
    th_mean = np.zeros(n_params)
    th_std = np.ones(n_params) * initial_std

    for iteration in range(n_iter):
        # Decaying extra noise (Szita &amp;amp; Lörincz 2006)
        noise_multiplier = max(1.0 - iteration / float(std_decay_time), 0)
        sample_std = np.sqrt(th_std + np.square(extra_std) * noise_multiplier)

        # Sample and evaluate
        thetas = th_mean + sample_std * np.random.randn(batch_size, n_params)
        rewards = np.array([evaluate_policy(env_name, th) for th in thetas])

        # Select elite and refit distribution
        elite_inds = rewards.argsort()[-n_elite:]
        elite_thetas = thetas[elite_inds]
        th_mean = elite_thetas.mean(axis=0)
        th_std = elite_thetas.var(axis=0)

        print(f&quot;Iter {iteration+1:3d} | Mean: {rewards.mean():6.1f} | Max: {rewards.max():.0f}&quot;)

    return th_mean

best_theta = cem('CartPole-v1', n_params=4)
# Iter   1 | Mean:   66.8 | Max: 500
# Iter  10 | Mean:  384.0 | Max: 500
# Iter  30 | Mean:  495.2 | Max: 500
# Iter  50 | Mean:  499.1 | Max: 500
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The population mean reward climbs from 67 to 499 in 50 iterations. Every single sample in the final batch scores near-perfect. Let's verify with 100 evaluation episodes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;scores = [evaluate_policy('CartPole-v1', best_theta) for _ in range(100)]
print(f&quot;Mean: {np.mean(scores):.0f} ± {np.std(scores):.0f}&quot;)
# Mean: 500 ± 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Perfect score. Four parameters, zero gradients, 50 iterations.&lt;/p&gt;
&lt;h3 id=&quot;what-just-happened-&quot;&gt;What Just Happened?&lt;/h3&gt;
&lt;p&gt;CEM works by maintaining a Gaussian distribution over policy parameters and repeatedly narrowing it toward the best-performing region. Each iteration has three steps:&lt;/p&gt;
&lt;h4 id=&quot;step-1-sample&quot;&gt;Step 1: Sample&lt;/h4&gt;
&lt;p&gt;We draw &lt;code&gt;batch_size=200&lt;/code&gt; parameter vectors from a Gaussian:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;thetas = th_mean + sample_std * np.random.randn(batch_size, n_params)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each &lt;code&gt;theta&lt;/code&gt; is a candidate policy. In iteration 1, the mean is zeros and the standard deviation is 1.0, so we're sampling random policies.&lt;/p&gt;
&lt;h4 id=&quot;step-2-evaluate-and-select&quot;&gt;Step 2: Evaluate and Select&lt;/h4&gt;
&lt;p&gt;We run each candidate policy on CartPole and rank them by total reward. Then we keep only the top 20% (the &quot;elite&quot; set):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;rewards = np.array([evaluate_policy(env_name, th) for th in thetas])
elite_inds = rewards.argsort()[-n_elite:]  # Top 40 out of 200
elite_thetas = thetas[elite_inds]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;step-3-refit-the-distribution&quot;&gt;Step 3: Refit the Distribution&lt;/h4&gt;
&lt;p&gt;We refit the Gaussian to match the elite samples:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;th_mean = elite_thetas.mean(axis=0)
th_std = elite_thetas.var(axis=0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The new mean moves toward parameters that performed well. The new variance shrinks because the elite samples cluster together. Next iteration, we sample from this tighter distribution, generating better candidates on average.&lt;/p&gt;
&lt;h4 id=&quot;watching-it-converge&quot;&gt;Watching It Converge&lt;/h4&gt;
&lt;p&gt;The training curve shows how the population improves:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(iters, mean_rewards, 'b-', alpha=0.7, label='Population mean')
ax.plot(iters, elite_mean_rewards, 'r-', linewidth=2, label='Elite mean')
ax.plot(iters, max_rewards, 'g--', alpha=0.5, label='Best in batch')
ax.axhline(y=500, color='k', linestyle=':', alpha=0.3, label='Max possible (500)')
ax.set_xlabel('Iteration')
ax.set_ylabel('Total Reward')
ax.legend()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://sesen.ai/images/posts/cross-entropy-method-evolution-style-rl-training-curve.webp&quot; alt=&quot;CEM training curve on CartPole-v1 showing population mean climbing from 67 to 500 over 50 iterations&quot;&gt;&lt;/p&gt;
&lt;p&gt;The elite mean hits 500 almost immediately (iteration 2). But the population mean takes longer to catch up because the distribution is still wide. By iteration 30, even randomly sampled policies from the learned distribution score near-perfect.&lt;/p&gt;
&lt;h4 id=&quot;the-distribution-narrows-over-time&quot;&gt;The Distribution Narrows Over Time&lt;/h4&gt;
&lt;p&gt;To see this visually, here's how the reward distribution across the 200 samples evolves:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;fig, axes = plt.subplots(1, 3, figsize=(12, 4))
for ax, iteration_rewards, title in zip(axes, selected_iterations, titles):
    ax.hist(iteration_rewards, bins=20, color='steelblue', edgecolor='white')
    ax.axvline(x=iteration_rewards.mean(), color='red', linestyle='--')
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://sesen.ai/images/posts/cross-entropy-method-evolution-style-rl-reward-distributions.webp&quot; alt=&quot;Reward distributions at iterations 1, 10, and 50 showing the population concentrating at 500&quot;&gt;&lt;/p&gt;
&lt;p&gt;In iteration 1, most policies fail quickly (reward &amp;lt; 100) with a few lucky ones reaching 500. By iteration 10, the distribution is bimodal: many policies near 500 but some still struggling. By iteration 50, the entire population clusters at 500. The distribution has collapsed onto the solution.&lt;/p&gt;
&lt;h3 id=&quot;going-deeper&quot;&gt;Going Deeper&lt;/h3&gt;
&lt;h4 id=&quot;the-noisy-cross-entropy-method&quot;&gt;The Noisy Cross-Entropy Method&lt;/h4&gt;
&lt;p&gt;The original CEM (Rubinstein 1999) has a failure mode: the variance can collapse to zero too quickly, trapping the search in a local optimum. Szita and Lörincz (2006) fixed this with the &quot;noisy&quot; variant that adds decaying extra variance:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://latex.codecogs.com/png.image?%5Cdpi%7B150%7D%5Csigma_%7Bt%2B1%7D%5E2%20%3D%20%5Csigma_%7Bt%2C%5Ctext%7Belite%7D%7D%5E2%20%2B%20Z_t%5E2%20%5Ccdot%20%5Csigma_%7B%5Ctext%7Bextra%7D%7D%5E2&quot; alt=&quot;equation&quot;&gt;&lt;/p&gt;
&lt;p&gt;where &lt;code&gt;$Z_t = \max(1 - t / T_{\text{decay}},\; 0)$&lt;/code&gt; decays linearly to zero. Early iterations get extra exploration; later iterations trust the elite variance.&lt;/p&gt;
&lt;p&gt;This is exactly what our code does:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;noise_multiplier = max(1.0 - iteration / float(std_decay_time), 0)
sample_std = np.sqrt(th_std + np.square(extra_std) * noise_multiplier)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;extra_std=0.5&lt;/code&gt; decays over &lt;code&gt;std_decay_time=25&lt;/code&gt; iterations. After iteration 25, the sampling distribution uses only the elite variance.&lt;/p&gt;
&lt;h4 id=&quot;hyperparameters&quot;&gt;Hyperparameters&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Parameter &lt;/th&gt;&lt;th&gt;Value &lt;/th&gt;&lt;th&gt;Effect&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;batch_size&lt;/code&gt; &lt;/td&gt;&lt;td&gt;200 &lt;/td&gt;&lt;td&gt;More samples = better coverage but slower per iteration&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;elite_frac&lt;/code&gt; &lt;/td&gt;&lt;td&gt;0.2 &lt;/td&gt;&lt;td&gt;Lower = more selective, faster convergence, risk of premature collapse&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;initial_std&lt;/code&gt; &lt;/td&gt;&lt;td&gt;1.0 &lt;/td&gt;&lt;td&gt;Too low = miss good regions; too high = waste samples on extreme policies&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;extra_std&lt;/code&gt; &lt;/td&gt;&lt;td&gt;0.5 &lt;/td&gt;&lt;td&gt;Noise injection; 0 = original CEM, &amp;gt;0 = noisy CEM&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;std_decay_time&lt;/code&gt; &lt;/td&gt;&lt;td&gt;25 &lt;/td&gt;&lt;td&gt;How many iterations before extra noise disappears&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The most sensitive parameter is &lt;code&gt;elite_frac&lt;/code&gt;. At 0.2 (keep top 40 of 200), we balance exploitation and exploration. Setting it to 0.01 (keep top 2) would converge faster in easy environments but collapse in hard ones.&lt;/p&gt;
&lt;h4 id=&quot;cem-vs-random-search&quot;&gt;CEM vs Random Search&lt;/h4&gt;
&lt;p&gt;Both CEM and random search sample 200 policies per iteration. The difference: random search starts fresh every time, while CEM builds on what worked:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(iters, cem_mean_rewards, 'b-', linewidth=2, label='CEM (population mean)')
ax.plot(iters, random_mean_rewards, 'r--', linewidth=2, label='Random search (mean)')
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://sesen.ai/images/posts/cross-entropy-method-evolution-style-rl-cem-vs-random.webp&quot; alt=&quot;CEM population mean climbing to 500 while random search stays flat at ~60&quot;&gt;&lt;/p&gt;
&lt;p&gt;Random search averages about 60 reward per iteration, forever. CEM reaches 500 because each iteration's distribution is informed by the last. The &quot;select and refit&quot; loop creates a directed search through parameter space.&lt;/p&gt;
&lt;h4 id=&quot;cem-vs-policy-gradients-vs-dqn&quot;&gt;CEM vs Policy Gradients vs DQN&lt;/h4&gt;
&lt;p&gt;How does CEM compare to the gradient-based methods we've covered?&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Method &lt;/th&gt;&lt;th&gt;What it optimises &lt;/th&gt;&lt;th&gt;Needs gradients? &lt;/th&gt;&lt;th&gt;Scales to large nets?&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;CEM &lt;/td&gt;&lt;td&gt;Policy parameters directly &lt;/td&gt;&lt;td&gt;No &lt;/td&gt;&lt;td&gt;Poorly (&amp;gt;1000 params)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://sesen.ai/blog/policy-gradients-reinforce-from-scratch&quot;&gt;REINFORCE&lt;/a&gt; &lt;/td&gt;&lt;td&gt;Policy parameters via log-prob gradient &lt;/td&gt;&lt;td&gt;Yes &lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://sesen.ai/blog/deep-q-networks-experience-replay-target-networks&quot;&gt;DQN&lt;/a&gt; &lt;/td&gt;&lt;td&gt;Value function (Q-values) &lt;/td&gt;&lt;td&gt;Yes &lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://sesen.ai/blog/q-learning-frozen-lake-from-scratch&quot;&gt;Q-Learning&lt;/a&gt; &lt;/td&gt;&lt;td&gt;Value function (Q-table) &lt;/td&gt;&lt;td&gt;No &lt;/td&gt;&lt;td&gt;No (tabular only)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;CEM's sweet spot: &lt;strong&gt;problems with fewer than ~1000 parameters&lt;/strong&gt; where you want a simple, parallelisable algorithm. For a 4-parameter linear policy on CartPole, CEM is hard to beat. For a million-parameter Atari network, you need &lt;a rel=&quot;nofollow&quot; href=&quot;https://sesen.ai/blog/policy-gradients-reinforce-from-scratch&quot;&gt;policy gradients&lt;/a&gt; or DQN.&lt;/p&gt;
&lt;h4 id=&quot;when-not-to-use-cem&quot;&gt;When NOT to Use CEM&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;High-dimensional parameter spaces.&lt;/strong&gt; CEM samples grow exponentially less effective as dimensions increase. A 1000-parameter network needs enormous batch sizes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Environments with sparse rewards.&lt;/strong&gt; If most policies score zero (e.g., Montezuma's Revenge), the elite set is just noise&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;When you need sample efficiency.&lt;/strong&gt; CEM uses 200 episodes per iteration vs REINFORCE using ~5 episodes per batch. If environment evaluations are expensive, gradient methods win&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Continuous action spaces with complex dynamics.&lt;/strong&gt; CEM with a linear policy can only learn linear decision boundaries. Problems requiring nonlinear policies need either a neural network (large parameter space) or a different algorithm&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;connection-to-genetic-algorithms&quot;&gt;Connection to Genetic Algorithms&lt;/h4&gt;
&lt;p&gt;If you read the &lt;a rel=&quot;nofollow&quot; href=&quot;https://sesen.ai/blog/genetic-algorithms-from-scratch&quot;&gt;genetic algorithms post&lt;/a&gt;, CEM will feel familiar. Both are population-based, derivative-free optimisation methods. The difference is in how they generate the next population:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Genetic algorithms&lt;/strong&gt; use crossover and mutation operators on individual solutions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CEM&lt;/strong&gt; fits a probability distribution to the elite set and samples from it&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CEM is sometimes called an &quot;estimation of distribution algorithm&quot; (EDA). Instead of recombining individual solutions, it models the structure of good solutions as a distribution and samples new candidates from that model. For real-valued parameter optimisation, this Gaussian model is often more effective than genetic crossover.&lt;/p&gt;
&lt;h3 id=&quot;where-this-comes-from&quot;&gt;Where This Comes From&lt;/h3&gt;
&lt;p&gt;The Cross-Entropy Method was introduced by &lt;strong&gt;Reuven Rubinstein&lt;/strong&gt; in his 1999 paper &lt;a rel=&quot;nofollow&quot; href=&quot;https://doi.org/10.1023/A:1010091220143&quot;&gt;&quot;The Cross-Entropy Method for Combinatorial and Continuous Optimization&quot;&lt;/a&gt;. The name comes from the original application: minimising the cross-entropy (KL divergence) between a reference distribution and the optimal importance sampling distribution for rare-event simulation.&lt;/p&gt;
&lt;h4 id=&quot;the-core-idea&quot;&gt;The Core Idea&lt;/h4&gt;
&lt;p&gt;Rubinstein's insight was that rare-event estimation and optimisation are essentially the same problem. To estimate &lt;code&gt;$P(S(X) \geq \gamma)$&lt;/code&gt; for a rare threshold &lt;code&gt;$\gamma$&lt;/code&gt;, you need to find a sampling distribution that concentrates on high-&lt;code&gt;$S(X)$&lt;/code&gt; regions. The CE method does this by iteratively:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Drawing samples from the current distribution &lt;code&gt;$f(\cdot;\, v_t)$&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Selecting the elite samples (those with &lt;code&gt;$S(X) \geq \gamma_t$&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Updating the distribution parameters to minimise the KL divergence to the empirical elite distribution&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For a Gaussian family, step 3 has a closed-form solution: the mean and variance of the elite samples. This is exactly what our implementation does.&lt;/p&gt;
&lt;h4 id=&quot;the-formal-algorithm&quot;&gt;The Formal Algorithm&lt;/h4&gt;
&lt;p&gt;From Rubinstein and Kroese (2004), the CEM update for a parametric family &lt;code&gt;$\{f(\cdot;\, v)\}$&lt;/code&gt; is:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://latex.codecogs.com/png.image?%5Cdpi%7B150%7Dv_%7Bt%2B1%7D%20%3D%20%5Carg%5Cmax_v%20%5Cfrac%7B1%7D%7BN%7D%20%5Csum_%7Bi%3D1%7D%5E%7BN%7D%20I%5C%7BS(X_i)%20%5Cgeq%20%5Cgamma_t%5C%7D%20%5Cln%20f(X_i%3B%5C%2C%20v)&quot; alt=&quot;equation&quot;&gt;&lt;/p&gt;
&lt;p&gt;where &lt;code&gt;$I\{\cdot\}$&lt;/code&gt; is the indicator function selecting elite samples. For a multivariate Gaussian with diagonal covariance, this yields:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://latex.codecogs.com/png.image?%5Cdpi%7B150%7D%5Cmu_%7Bt%2B1%7D%20%3D%20%5Cfrac%7B1%7D%7BN_%7B%5Ctext%7Belite%7D%7D%7D%20%5Csum_%7Bi%20%5Cin%20%5Ctext%7Belite%7D%7D%20X_i%2C%20%5Cquad%20%5Csigma_%7Bt%2B1%7D%5E2%20%3D%20%5Cfrac%7B1%7D%7BN_%7B%5Ctext%7Belite%7D%7D%7D%20%5Csum_%7Bi%20%5Cin%20%5Ctext%7Belite%7D%7D%20(X_i%20-%20%5Cmu_%7Bt%2B1%7D)%5E2&quot; alt=&quot;equation&quot;&gt;&lt;/p&gt;
&lt;p&gt;The sample mean and variance of the elite set. Elegantly simple.&lt;/p&gt;
&lt;h4 id=&quot;from-rare-events-to-tetris&quot;&gt;From Rare Events to Tetris&lt;/h4&gt;
&lt;p&gt;The method found its way into reinforcement learning through &lt;strong&gt;Szita and Lörincz (2006)&lt;/strong&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://doi.org/10.1162/neco.2006.18.12.2936&quot;&gt;&quot;Learning Tetris Using the Noisy Cross-Entropy Method&quot;&lt;/a&gt;. They made two key modifications for RL:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Noisy updates&lt;/strong&gt;: Adding decaying extra variance to prevent premature convergence (the &lt;code&gt;extra_std&lt;/code&gt; parameter in our code)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Direct policy search&lt;/strong&gt;: Treating the policy's weight vector as the parameter to optimise, with episode return as the objective function&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;The noisy cross-entropy method adds a time-decreasing noise term to avoid premature convergence of the variance to zero.&quot;&lt;br&gt;
&lt;em&gt;Szita and Lörincz (2006)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Their noisy CEM achieved record-breaking performance on Tetris at the time, outperforming methods that required orders of magnitude more computation. Our implementation follows their variant faithfully, including the linear noise decay schedule described in Section 3 of their paper.&lt;/p&gt;
&lt;h4 id=&quot;theoretical-properties&quot;&gt;Theoretical Properties&lt;/h4&gt;
&lt;p&gt;Unlike policy gradient methods, CEM has no convergence guarantees to a local optimum. It is a heuristic. However, it has practical advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Embarrassingly parallel&lt;/strong&gt;: All 200 evaluations per iteration are independent&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No reward shaping needed&lt;/strong&gt;: Works with any scalar objective, even non-differentiable ones&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Robust to noisy evaluations&lt;/strong&gt;: The elite selection acts as a natural filter&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The method's simplicity is also its limitation. As Rubinstein and Kroese note, the Gaussian parametric family assumes the optimal parameter region is unimodal. Multi-modal reward landscapes can trap CEM in a single mode.&lt;/p&gt;
&lt;h4 id=&quot;further-reading&quot;&gt;Further Reading&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://doi.org/10.1023/A:1010091220143&quot;&gt;Rubinstein (1999)&lt;/a&gt;, &quot;The Cross-Entropy Method for Combinatorial and Continuous Optimization&quot; - The original CE method paper&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://doi.org/10.1007/978-1-4757-4321-0&quot;&gt;Rubinstein and Kroese (2004)&lt;/a&gt;, &lt;em&gt;The Cross-Entropy Method: A Unified Approach to Combinatorial Optimization, Monte Carlo Simulation, and Machine Learning&lt;/em&gt; - The comprehensive textbook&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://doi.org/10.1162/neco.2006.18.12.2936&quot;&gt;Szita and Lörincz (2006)&lt;/a&gt;, &quot;Learning Tetris Using the Noisy Cross-Entropy Method&quot; - The noisy variant for RL&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://arxiv.org/abs/1703.03864&quot;&gt;Salimans et al. (2017)&lt;/a&gt;, &quot;Evolution Strategies as a Scalable Alternative to Reinforcement Learning&quot; - Modern evolution strategies at scale (OpenAI)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sutton and Barto (2018)&lt;/strong&gt;, Ch. 13 - Policy gradient methods for comparison&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;try-it-yourself&quot;&gt;Try It Yourself&lt;/h3&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://colab.research.google.com/github/zhubarb/sesen_ai_ml_tutorials/blob/main/notebooks/reinforcement-learning/cross_entropy_method.ipynb&quot;&gt;interactive notebook&lt;/a&gt; includes exercises:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Elite fraction sweep&lt;/strong&gt;: Try &lt;code&gt;elite_frac&lt;/code&gt; values of 0.01, 0.1, 0.2, and 0.5. How does selectivity affect convergence speed and stability?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Noisy vs vanilla CEM&lt;/strong&gt;: Set &lt;code&gt;extra_std=0&lt;/code&gt; and compare convergence. Does the noisy variant help on CartPole, or only on harder problems?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Neural network policy&lt;/strong&gt;: Replace the linear policy with a small neural net (8 hidden units). How many CEM iterations does it take to solve CartPole now? At what network size does CEM become impractical?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Different environments&lt;/strong&gt;: Try CEM on &lt;code&gt;Acrobot-v1&lt;/code&gt; or &lt;code&gt;MountainCar-v0&lt;/code&gt;. Which environments does CEM handle well, and which expose its limitations?&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;interactive-tools&quot;&gt;Interactive Tools&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://sesen.ai/q-learning-visualizer&quot;&gt;Q-Learning Visualiser&lt;/a&gt; — See value-based RL in action and compare it with the policy search approach of CEM&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;related-posts&quot;&gt;Related Posts&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://sesen.ai/blog/genetic-algorithms-from-scratch&quot;&gt;Genetic Algorithms: From Line Fitting to the Travelling Salesman&lt;/a&gt; - Another population-based, derivative-free optimisation method. CEM replaces crossover and mutation with distribution fitting.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://sesen.ai/blog/policy-gradients-reinforce-from-scratch&quot;&gt;Policy Gradients: REINFORCE from Scratch with NumPy&lt;/a&gt; - The gradient-based alternative for policy search. Uses backpropagation through the policy, which scales to large networks but requires differentiable objectives.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://sesen.ai/blog/deep-q-networks-experience-replay-target-networks&quot;&gt;Deep Q-Networks: Experience Replay and Target Networks&lt;/a&gt; - Value-based RL with neural networks. A fundamentally different approach that learns what states are valuable rather than directly searching for good policies.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;frequently-asked-questions&quot;&gt;Frequently Asked Questions&lt;/h3&gt;
&lt;h4 id=&quot;why-is-the-cross-entropy-method-called-cross-entropy-if-it-does-not-use-a-loss-function-&quot;&gt;Why is the Cross-Entropy Method called &quot;cross-entropy&quot; if it does not use a loss function?&lt;/h4&gt;
&lt;p&gt;The name comes from the original application in rare-event simulation, where the algorithm minimises the cross-entropy (KL divergence) between the current sampling distribution and the optimal importance sampling distribution. In the reinforcement learning context, the name persists even though the update reduces to simply computing the mean and variance of the elite samples.&lt;/p&gt;
&lt;h4 id=&quot;how-does-cem-compare-to-random-search-&quot;&gt;How does CEM compare to random search?&lt;/h4&gt;
&lt;p&gt;Both methods sample candidate policies each iteration, but random search draws from a fixed distribution every time, while CEM updates its distribution based on the best-performing candidates. This directed search means CEM builds on previous successes, converging to good solutions far faster than random search on problems with structure to exploit.&lt;/p&gt;
&lt;h4 id=&quot;can-cem-solve-problems-with-continuous-action-spaces-&quot;&gt;Can CEM solve problems with continuous action spaces?&lt;/h4&gt;
&lt;p&gt;CEM can optimise over continuous policy parameters, but the policy itself determines how actions are generated. A linear policy with CEM-optimised weights can only produce binary or discrete decisions. For truly continuous action spaces with complex dynamics, you would need a more expressive policy architecture, which increases the parameter count and makes CEM less practical.&lt;/p&gt;
&lt;h4 id=&quot;what-is-the-role-of-the-elite-fraction-hyperparameter-&quot;&gt;What is the role of the elite fraction hyperparameter?&lt;/h4&gt;
&lt;p&gt;The elite fraction controls how selective the algorithm is when choosing which candidates inform the next distribution. A smaller fraction (e.g. 0.01) converges faster but risks collapsing onto a local optimum. A larger fraction (e.g. 0.5) explores more broadly but converges more slowly. A value around 0.2 is a common default that balances exploitation and exploration.&lt;/p&gt;
&lt;h4 id=&quot;why-does-the-noisy-cem-variant-add-extra-variance-that-decays-over-time-&quot;&gt;Why does the noisy CEM variant add extra variance that decays over time?&lt;/h4&gt;
&lt;p&gt;Without extra variance, the sampling distribution can collapse to near-zero variance too quickly, trapping the search around a potentially suboptimal solution. The decaying noise keeps exploration alive in early iterations when the algorithm is still uncertain about the best region, then gradually disappears to allow precise convergence in later iterations.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24679/the-cross-entropy-method-solving-rl-without-gradients</guid>
<pubDate>Sat, 15 Aug 2026 11:04:52 +0000</pubDate>
</item>
</channel>
</rss>