Current section
Files
Jump to
Current section
Files
lib/oorl_reward_learning.ex
defmodule OORL.RewardLearning do
@moduledoc """
OORL Reward Learning module implementing mathematical reward combination algorithms
as specified in AAOS Section 6.
Provides multiple reward combination strategies:
- Linear combination
- Weighted combination
- Adaptive combination
- Hierarchical combination
Maintains mathematical properties including Lipschitz continuity and bounded learning.
"""
defstruct [
:combination_strategy,
:weights,
:adaptation_rate,
:lipschitz_constant,
:reward_history,
:intrinsic_components,
:extrinsic_components
]
@type reward_combination_strategy :: :linear | :weighted | :adaptive | :hierarchical
@type reward_component :: %{
type: :task_reward | :social_reward | :curiosity_reward | :intrinsic_reward,
value: float(),
confidence: float(),
source: String.t()
}
@doc """
Combines multiple reward components using the specified strategy.
## Parameters
- `extrinsic_rewards`: List of external reward components
- `intrinsic_rewards`: List of internal reward components
- `strategy`: Combination strategy to use
## Returns
`{:ok, combined_reward}` or `{:error, reason}`
## Examples
iex> OORL.RewardLearning.combine_rewards([%{type: :task_reward, value: 0.8}],
...> [%{type: :curiosity_reward, value: 0.3}], :linear)
{:ok, 1.1}
"""
def combine_rewards(extrinsic_rewards, intrinsic_rewards, strategy \\ :linear) do
try do
case strategy do
:linear ->
{:ok, linear_combination(extrinsic_rewards, intrinsic_rewards)}
:weighted ->
{:ok, weighted_combination(extrinsic_rewards, intrinsic_rewards)}
:adaptive ->
{:ok, adaptive_combination(extrinsic_rewards, intrinsic_rewards)}
:hierarchical ->
{:ok, hierarchical_combination(extrinsic_rewards, intrinsic_rewards)}
_ ->
{:error, "Unknown combination strategy: #{strategy}"}
end
rescue
error ->
{:error, "Reward combination failed: #{inspect(error)}"}
end
end
@doc """
Creates a new reward learning configuration.
## Parameters
- `opts`: Configuration options including strategy, weights, adaptation_rate
## Returns
`%OORL.RewardLearning{}` struct
"""
def new(opts \\ []) do
%OORL.RewardLearning{
combination_strategy: Keyword.get(opts, :strategy, :adaptive),
weights: Keyword.get(opts, :weights, default_weights()),
adaptation_rate: Keyword.get(opts, :adaptation_rate, 0.01),
lipschitz_constant: Keyword.get(opts, :lipschitz_constant, 1.0),
reward_history: [],
intrinsic_components: Keyword.get(opts, :intrinsic_components, [:curiosity_reward]),
extrinsic_components: Keyword.get(opts, :extrinsic_components, [:task_reward, :social_reward])
}
end
@doc """
Adapts reward weights based on performance feedback.
## Parameters
- `reward_learner`: Current reward learning configuration
- `performance_metrics`: Performance feedback data
## Returns
Updated `%OORL.RewardLearning{}` struct
"""
def adapt_weights(reward_learner, performance_metrics) do
new_weights = case reward_learner.combination_strategy do
:adaptive ->
update_adaptive_weights(reward_learner.weights, performance_metrics, reward_learner.adaptation_rate)
:hierarchical ->
update_hierarchical_weights(reward_learner.weights, performance_metrics)
_ ->
reward_learner.weights
end
%{reward_learner |
weights: new_weights,
reward_history: [performance_metrics | Enum.take(reward_learner.reward_history, 99)]
}
end
@doc """
Validates that reward function maintains mathematical properties.
## Parameters
- `reward_function`: Function to validate
- `test_points`: Sample points for validation
## Returns
`{:ok, validation_results}` with properties like Lipschitz continuity
"""
def validate_mathematical_properties(reward_function, test_points) do
lipschitz_violations = check_lipschitz_continuity(reward_function, test_points)
boundedness_check = check_boundedness(reward_function, test_points)
monotonicity_check = check_monotonicity(reward_function, test_points)
validation_results = %{
lipschitz_violations: lipschitz_violations,
is_bounded: boundedness_check,
is_monotonic: monotonicity_check,
total_violations: length(lipschitz_violations),
compliance_score: calculate_compliance_score(lipschitz_violations, boundedness_check, monotonicity_check)
}
{:ok, validation_results}
end
# Private implementation functions
defp linear_combination(extrinsic_rewards, intrinsic_rewards) do
extrinsic_sum = Enum.reduce(extrinsic_rewards, 0, fn reward, acc ->
acc + Map.get(reward, :value, 0)
end)
intrinsic_sum = Enum.reduce(intrinsic_rewards, 0, fn reward, acc ->
acc + Map.get(reward, :value, 0)
end)
extrinsic_sum + intrinsic_sum
end
defp weighted_combination(extrinsic_rewards, intrinsic_rewards) do
weights = default_weights()
extrinsic_weighted = Enum.reduce(extrinsic_rewards, 0, fn reward, acc ->
weight = Map.get(weights, reward[:type], 1.0)
acc + (reward[:value] * weight)
end)
intrinsic_weighted = Enum.reduce(intrinsic_rewards, 0, fn reward, acc ->
weight = Map.get(weights, reward[:type], 0.3)
acc + (reward[:value] * weight)
end)
extrinsic_weighted + intrinsic_weighted
end
defp adaptive_combination(extrinsic_rewards, intrinsic_rewards) do
# Adaptive weights based on recent performance
base_weights = default_weights()
# Increase intrinsic weight if extrinsic rewards are sparse
extrinsic_count = length(extrinsic_rewards)
intrinsic_boost = if extrinsic_count < 2, do: 1.5, else: 1.0
extrinsic_weighted = Enum.reduce(extrinsic_rewards, 0, fn reward, acc ->
weight = Map.get(base_weights, reward[:type], 1.0)
acc + (reward[:value] * weight)
end)
intrinsic_weighted = Enum.reduce(intrinsic_rewards, 0, fn reward, acc ->
weight = Map.get(base_weights, reward[:type], 0.3) * intrinsic_boost
acc + (reward[:value] * weight)
end)
extrinsic_weighted + intrinsic_weighted
end
defp hierarchical_combination(extrinsic_rewards, intrinsic_rewards) do
# Hierarchical combination with priority levels
high_priority = Enum.filter(extrinsic_rewards, fn r -> Map.get(r, :priority, :medium) == :high end)
medium_priority = Enum.filter(extrinsic_rewards, fn r -> Map.get(r, :priority, :medium) == :medium end)
high_sum = Enum.reduce(high_priority, 0, fn r, acc -> acc + r[:value] end) * 2.0
medium_sum = Enum.reduce(medium_priority, 0, fn r, acc -> acc + r[:value] end) * 1.0
intrinsic_sum = Enum.reduce(intrinsic_rewards, 0, fn r, acc -> acc + r[:value] end) * 0.5
high_sum + medium_sum + intrinsic_sum
end
defp default_weights do
%{
task_reward: 1.0,
social_reward: 0.6,
curiosity_reward: 0.4,
intrinsic_reward: 0.3,
exploration_reward: 0.2,
collaboration_reward: 0.5
}
end
defp update_adaptive_weights(current_weights, performance_metrics, adaptation_rate) do
performance_score = Map.get(performance_metrics, :overall_score, 0.5)
# Increase weights for components that contributed to good performance
Enum.reduce(current_weights, %{}, fn {component, weight}, acc ->
component_contribution = Map.get(performance_metrics, component, 0.5)
# Adaptive update based on contribution
adjustment = adaptation_rate * (component_contribution - 0.5) * performance_score
new_weight = max(0.1, min(2.0, weight + adjustment))
Map.put(acc, component, new_weight)
end)
end
defp update_hierarchical_weights(current_weights, performance_metrics) do
# Hierarchical weights adapt based on goal achievement
goal_achievement = Map.get(performance_metrics, :goal_achievement, 0.5)
if goal_achievement > 0.8 do
# Increase exploration when performing well
Map.merge(current_weights, %{curiosity_reward: 0.6, exploration_reward: 0.4})
else
# Focus on exploitation when underperforming
Map.merge(current_weights, %{task_reward: 1.2, social_reward: 0.8})
end
end
defp check_lipschitz_continuity(reward_function, test_points) do
lipschitz_constant = 1.0
violations = for {point1, point2} <- point_pairs(test_points),
violation = check_lipschitz_pair(reward_function, point1, point2, lipschitz_constant),
violation != nil do
violation
end
violations
end
defp check_lipschitz_pair(reward_function, point1, point2, lipschitz_constant) do
try do
reward1 = apply_reward_function(reward_function, point1)
reward2 = apply_reward_function(reward_function, point2)
distance = euclidean_distance(point1, point2)
reward_diff = abs(reward1 - reward2)
if reward_diff > lipschitz_constant * distance do
%{point1: point1, point2: point2, violation_magnitude: reward_diff - lipschitz_constant * distance}
else
nil
end
rescue
_ -> nil
end
end
defp check_boundedness(reward_function, test_points) do
rewards = Enum.map(test_points, fn point ->
apply_reward_function(reward_function, point)
end)
min_reward = Enum.min(rewards)
max_reward = Enum.max(rewards)
# Check if rewards are reasonably bounded
min_reward >= -10.0 && max_reward <= 10.0
end
defp check_monotonicity(reward_function, test_points) do
# Simple monotonicity check for ordered test points
ordered_points = Enum.sort(test_points)
rewards = Enum.map(ordered_points, fn point ->
apply_reward_function(reward_function, point)
end)
# Check if rewards are non-decreasing
rewards
|> Enum.chunk_every(2, 1, :discard)
|> Enum.all?(fn [a, b] -> b >= a end)
end
defp apply_reward_function(reward_function, point) when is_function(reward_function) do
reward_function.(point)
end
defp apply_reward_function(reward_function, point) when is_map(reward_function) do
# Mock implementation for map-based reward functions
Map.get(reward_function, :base_reward, 0.0) + :rand.uniform() * 0.1
end
defp euclidean_distance(point1, point2) when is_number(point1) and is_number(point2) do
abs(point1 - point2)
end
defp euclidean_distance(point1, point2) when is_list(point1) and is_list(point2) do
Enum.zip(point1, point2)
|> Enum.reduce(0, fn {a, b}, acc -> acc + (a - b) * (a - b) end)
|> :math.sqrt()
end
defp euclidean_distance(_point1, _point2), do: 1.0
defp point_pairs(test_points) do
for i <- 0..(length(test_points) - 2),
j <- (i + 1)..(length(test_points) - 1) do
{Enum.at(test_points, i), Enum.at(test_points, j)}
end
end
defp calculate_compliance_score(lipschitz_violations, is_bounded, is_monotonic) do
violation_penalty = length(lipschitz_violations) * 0.1
boundedness_bonus = if is_bounded, do: 0.3, else: 0.0
monotonicity_bonus = if is_monotonic, do: 0.2, else: 0.0
max(0.0, 1.0 - violation_penalty + boundedness_bonus + monotonicity_bonus)
end
end