Home▸Articles▸Generative Models

De Novo Drug Design with Reinforcement Learning

Reinforcement learning agents can sample from chemical space while optimising towards multi-objective drug-like reward functions. This guide covers the REINVENT architecture (prior + agent RNN + RL), reward component design, multi-objective aggregation, avoiding mode collapse and reward hacking, and evaluating generated libraries against GuacaMol and MOSES benchmarks.

mysimulator teamUpdated January 2026≈ 5 min read▶ Open the simulation

De novo RL drug design treats molecule generation as a sequential decision-making problem. A generative model (SMILES RNN, transformer, or graph model) is used as a policy; the RL algorithm updates this policy to maximise expected reward. REINVENT (Olivecrona et al., 2017; Blaschke et al., 2020; REINVENT4, 2024) is the most widely deployed framework in industry, using a PPO/REINFORCE variant with an augmented memory and diversity filter to prevent mode collapse.

REINVENT Architecture Overview

Reward Function Design

from rdkit import Chem
from rdkit.Chem import Descriptors, QED
from rdkit.Chem import RDConfig
import os
import sys
sys.path.append(os.path.join(RDConfig.RDContribDir, "SA_Score"))
import sascorer  # RDKit SA score contribution

from dataclasses import dataclass
from typing import Callable


@dataclass
class ScoringComponent:
    """Single scoring component with a weight and transform function."""
    name: str
    weight: float
    score_fn: Callable[[Chem.Mol], float]
    min_val: float = 0.0
    max_val: float = 1.0

    def score(self, mol: Chem.Mol) -> float:
        """Return normalised score in [0, 1]."""
        raw = self.score_fn(mol)
        normalised = (raw - self.min_val) / (self.max_val - self.min_val)
        return float(max(0.0, min(1.0, normalised)))


def qed_score(mol: Chem.Mol) -> float:
    return QED.qed(mol)


def sa_score_component(mol: Chem.Mol) -> float:
    """SA score ranges 1 (easy) to 10 (hard). Invert and normalise."""
    sa = sascorer.calculateScore(mol)
    return 1.0 - (sa - 1.0) / 9.0  # -> 1 is easy, 0 is very hard


def mw_component(mol: Chem.Mol) -> float:
    """Prefer MW in 300–500 Da. Returns 1.0 in range, degrades outside."""
    mw = Descriptors.MolWt(mol)
    if 300 <= mw <= 500:
        return 1.0
    elif mw < 300:
        return mw / 300.0
    else:
        return max(0.0, 1.0 - (mw - 500) / 300.0)


def activity_model_score(mol: Chem.Mol, activity_model) -> float:
    """Predicted pIC50 normalised to [0, 1] (clipped to 4–9 range)."""
    from rdkit.Chem import AllChem
    fp = AllChem.GetMorganFingerprintAsBitVect(mol, 2, 2048)
    pIC50 = float(activity_model.predict([list(fp)])[0])
    return (pIC50 - 4.0) / 5.0  # 4 = inactive, 9 = highly active


class MultiObjectiveScoringFunction:
    """
    Weighted geometric mean of scoring components.
    Geometric mean penalises zero-scores harder than arithmetic mean,
    preventing reward hacking via single-component exploitation.
    """

    def __init__(self, components: list[ScoringComponent]):
        self.components = components
        total_weight = sum(c.weight for c in components)
        # Normalise weights to sum to 1
        for c in components:
            c.weight = c.weight / total_weight

    def __call__(self, smiles: str) -> float:
        mol = Chem.MolFromSmiles(smiles)
        if mol is None:
            return 0.0

        weighted_log_sum = 0.0
        for component in self.components:
            s = component.score(mol)
            s = max(s, 1e-6)  # Avoid log(0)
            weighted_log_sum += component.weight * float.__add__(0, __import__('math').log(s))

        import math
        return float(math.exp(weighted_log_sum))  # Geometric mean


# Example setup
components = [
    ScoringComponent("QED",       weight=1.0, score_fn=qed_score,       min_val=0.0, max_val=1.0),
    ScoringComponent("SA_Score",  weight=1.0, score_fn=sa_score_component, min_val=0.0, max_val=1.0),
    ScoringComponent("MW",        weight=0.5, score_fn=mw_component,    min_val=0.0, max_val=1.0),
]
scoring_fn = MultiObjectiveScoringFunction(components)

Diversity Filter and Scaffold Tracking

from rdkit.Chem.Scaffolds import MurckoScaffold
from collections import defaultdict

class DiversityFilter:
    """
    Penalise oversampling of the same Murcko scaffold.
    Molecules from a scaffold sampled more than `minscore_scaffold_count`
    times receive a diversity penalty.
    """

    def __init__(self, minscore: float = 0.4, scaffold_memory_size: int = 1000):
        self.minscore = minscore
        self.scaffold_memory_size = scaffold_memory_size
        self.scaffold_memory: dict[str, int] = defaultdict(int)

    def penalty(self, smiles: str, raw_score: float) -> float:
        if raw_score < self.minscore:
            return raw_score  # Don't penalise already-low scorers

        mol = Chem.MolFromSmiles(smiles)
        if mol is None:
            return 0.0

        scaffold_smiles = MurckoScaffold.MurckoScaffoldSmilesFromSmiles(smiles)
        self.scaffold_memory[scaffold_smiles] += 1

        count = self.scaffold_memory[scaffold_smiles]
        # Penalise proportionally to excess count above threshold (3)
        if count > 3:
            penalty = min(1.0, (count - 3) * 0.1)
            return raw_score * (1.0 - penalty)

        return raw_score

    def top_scaffolds(self, n: int = 10) -> list[tuple[str, int]]:
        return sorted(self.scaffold_memory.items(), key=lambda x: x[1], reverse=True)[:n]

GuacaMol and MOSES Benchmarks

BenchmarkKey MetricsWhat It Tests
GuacaMolValidity, Uniqueness, Novelty, KL Divergence, FCD, Goal-DirectedDistribution learning + goal-directed generation
MOSESValidity, Uniqueness, Novelty, Filters (PAINS), SNN, Frag, ScafDistribution-focused; proximity to training set
REINVENT4 internalFraction active (predicted), Mean score, Scaffold diversityCampaign-specific optimisation progress

Avoiding Reward Hacking

Key Takeaways

Try it live

Explore a related model hands-on: open AI Drug Design: Molecular Generation, Docking & ADMET Pipeline and change its parameters while it runs. Everything stays in your browser.

▶ Open AI Drug Design: Molecular Generation, Docking & ADMET Pipeline simulation

What did you find?

Add reproduction steps (optional)