LiteEFG
Python library for solving Extensive-form Games and implementation of various baseline algorithms (e.g., CFR, CFR+, PCFR, DOMD)
Install / Use
/learn @liumy2010/LiteEFGREADME
<h1 align="center">LiteEFG</h1>
<p align="center"> An effective and easy-to-use library for solving Extensive-form games. </p>Table of Contents
About the Project
LiteEFG is an efficient Python library for solving Extensive-form games (EFGs) based on computation graph. By defining the local update-rule for an information set (infoset) with Python, LiteEFG will automatically distribute the computation graph to all infosets of the game. Compared to implementing purely by Python, LiteEFG can provide up to 100x aaceleration.
Speed of LiteEFG
To demonstrate the efficiency of LiteEFG, the computation time of running Counterfactual Regret Minimization (CFR) [1] for $100,000$ iterations, without sampling, in the following environments are provided.
The results are tested on Ubuntu with CPU: 13th Gen Intel(R) Core(TM) i7-13700K 3.40 GHz.
- Kuhn Poker: $\leq 0.5s$
- Leduc Poker: $10s$
- Goofspiel: $10s$
- 4-sided Liar's Dice: $45s$
- Liar's Dice: $2$ hours
Installation
pip install LiteEFG
or
git clone --recursive https://github.com/liumy2010/LiteEFG.git
cd LiteEFG
pip install .
Prerequisites
- open_spiel >= 1.2
- A compiler with C++17 support
- Pip 10+ or CMake >= 3.4 (or 3.14+ on Windows, which was the first version to support VS 2019)
- Ninja or Pip 10+
Usage
Structure of LiteEFG
To use LiteEFG, the user first need to specify the computation graph of the algorithm. The computation graph has 2 parts: backward, and forward.
Graphs
- Backward Graph. LiteEFG will enumerate the infosets in the reversed breadth-first order of infosets, and the computation defined in backward graph will be executed. The variables of backward graph are stored locally in each infoset
- Forward Graph. LiteEFG will enumerate the infosets in the breadth-first order of infosets, and the computation defined in forward graph will be executed. The variables of forward graph are stored locally in each infoset
To define graph nodes for backward / forward graph, user may use the following two class
LiteEFG.backward(is_static=False, color=0): Within thewithstatement of this class, all graph nodes in the following will be defined in the backward graphis_staticindicates whether the graph node is static. If it is, then it will only be updated at initialization, otherwise update every time when callingEnvironment.updatecolor: Indicates the color of the nodes defined below. Then, during each update, the user may specify the node color that will be updated
LiteEFG.forward(is_static=False, color=0): Within thewithstatement of this class, all graph nodes in the following will be defined in the forward graph
Here's an example of computing the Fibonacci sequence.
with LiteEFG.backward(is_static=True):
a = LiteEFG.const(size=1, val=1.0)
b = LiteEFG.const(size=1, val=1.0)
with LiteEFG.backward(is_static=False):
c = a + b
a.inplace(b.copy()) # change the value of a to b
b.inplace(c.copy()) # change the value of b to c
# The details of inplace and copy can be found in
# the following
Order of Update
- Initialize. The static computation will be executed once when initialize the environment. The order is still
backward->forward. Initialization will be automatically done when callingenv.set_graph(graph) - Update. At every iteration when calling
environment.update, the backward graph will be updated first. Then, the forward graph will be updated
Default Variables
LiteEFG prepare several default variables for easier implementation of algorithms. Note that all variables are stored locally for each infoset.
utility: Updated every iteration. It stores the expected utility of each action of an infoset. For a two-player zero-sum game $\max_{\mathbf{x}}\min_{\mathbf{y}}\mathbf{x^\top A y}$, where $\mathbf{A}$ is the utility matrix,utility=$(\mathbf{Ay}^{tr})_{(I,a)}$ for max-player $\mathbf{x}$. $(\mathbf{x}^{tr}, \mathbf{y}^{tr})$ is the strategy fed intoenv.updateaction_set_size: The size of action set of an infosetreach_prob: The reach probabiltiy of player $p$ of an infoset, where $p$ is the player to take actions at that infosetopponent_reach_prob: The reach probability of players other than $p$ of an infosetsubtree_size: A vector with sizeaction_set_size, which stores the number of terminal (infoset, action) pairs in the subtree when taking an action
Operations
Basic Operations
LiteEFG.set_seed: Set the seed for the pseudo-random number generatorLiteEFG.random.uniform(x, lower=0.0, upper=1.0): Return a node with the same shape asxwhose elements are sampled from uniform distribution in[lower, upper)LiteEFG.random.normal(x, mean=0.0, stddev=1.0): Return a node with the same shape asxwhose elements are sampled from normal distributionLiteEFG.random.exponential(x, lambda_=1.0): Return a node with the same shape asxwhose elements are sampled from exponential distribution with ratelambda_GraphNode.inplace(expression): The value of the expression will be stored to the original address of GraphNodeGraphNode.sum() / LiteEFG.sum(x): Return sum of all elements ofGraphNodeGraphNode.mean() / LiteEFG.mean(x): Return mean of all elements ofGraphNodeGraphNode.max() / LiteEFG.max(x): Return max of all elements ofGraphNodeGraphNode.min() / LiteEFG.min(x): Return min of all elements ofGraphNodeGraphNode.copy(): Return the copy ofGraphNodeGraphNode.exp() / LiteEFG.exp(x): Return exponential of all elements ofGraphNode. For every index $i$, let $x_i\to e^{x_i}$GraphNode.log() / LiteEFG.log(x): Return log of all elements ofGraphNode. For every index $i$, let $x_i\to \ln(x_i)$GraphNode.argmax() / LiteEFG.argmax(x): Return argmax ofGraphNodeGraphNode.argmin() / LiteEFG.argmin(x): Return argmin ofGraphNodeGraphNode.euclidean() / LiteEFG.euclidean(x): Return $\frac{1}{2}\sum_i x_i^2$, where $\mathbf{x}$ is the vector stored atGraphNodeGraphNode.negative_entropy(shifted=False) / LiteEFG.negative_entropy(x, shifted=False): Return $\sum_i x_i\ln(x_i)+\mathbf{1}(\text{shifted})\log N$, where $\mathbf{x}\in\mathbf{R}^N$ is the vector stored atGraphNodeGraphNode.normalize(p_norm, ignore_negative=False) / LiteEFG.normalize(x, p_norm, ignore_negative=False): Normalize $\mathbf{x}$ by its $p$-norm. Specifically, $x_i\to \frac{x_i}{(\sum_j |x_j|^p)^{1/p}}$. Whenignore_negative=True, $x_i\to \frac{[x_i]^+}{(\sum_j ([x_j]^+)^p)^{1/p}}$, where $[x_i]^+=x_i\cdot \mathbf{1}(x_i\geq 0)$.GraphNode.dot(y) / LiteEFG.dot(x, y): Return $\langle \mathbf{x}, \mathbf{y}\rangle$GraphNode.maximum(y) / LiteEFG.maximum(x, y): Return $\mathbf{z}$ with $z_i=\max(x_i,y_i)$. Also supportsGraphNode.maximum(scalar) / LiteEFG.maximum(x, scalar)GraphNode.minimum(y) / LiteEFG.minimum(x, y): Return $\mathbf{z}$ with $z_i=\min(x_i,y_i)$. Also supportsGraphNode.minimum(scalar) / LiteEFG.minimum(x, scalar)x**y:yshould be a scalar and it will return $(x_1^y, x_2^y, ..., x_n^y)$LiteEFG.cat(nodes_list: list): Concatenate all nodes in the nodes_list
Game Specific Operations
GraphNode.project(distance_name : ["L2", "KL"], gamma=0.0, mu=uniform_distribution): Project $\mathbf{x}\in\mathbf{R}^N$ to the perturbed simplex $\Delta_N:=\{\mathbf{v}\succeq \gamma\mathbf{\mu}\colon \sum_i v_i=1\}$, with respect to either Euclidean distance orKL-Divergence. By default, $\gamma=0.0$ and $\mathbf{\mu}=\frac{1}{N}\mathbf{1}$LiteEFG.project(x, distance_name, gamma=0.0, mu=uniform_distribution): Returnx.project(distance_name, gamma, mu)LiteEFG.aggregate(x, aggregator_name : ["sum", "mean", "max", "min"], object_name : ["children", "parent"]="children", player : ["self", "opponents"]="self", padding=0.0): Aggregate variables from eitherobject_nameinfoset. By default,object_name="children"object_name="children": For each actionaof infosetI, the vector stored inGraphNodexof subsequent infosets under(I,a)will be concatenated into one vector. Then, the vector will be aggregated viaaggregator_name. At last, the resulted scalars of each(I,a)pair will be concatenated again and returned. Therefore, the returnedGraphNodestores a vector of sizeaction_set_size. If there's no subsequent infoset under an(I,a)pair, usepaddingas the scalar for(I,a)object_name="parent": AssumeIis a children of infoset-action pair(I',a'). If the vector stored atGraphNodexis of sizeaction_set_sizeof infosetI', then its $(a')^{th}$ component will be returned. Otherwise, ifxis simply a scalar,xwill be returned. If no parent exists, returnpaddingplayerindicates whether to aggregate infosets belong to the player herself or aggregate infosets belong to opponents. An example can be found inLiteEFG/baselines/QFR.py
Environments
Environment.update(strategies, upd_player=-1, upd_color=[-1], traverse_type="default"): Update the computation graph stored in the environment.strategiesis a list of lengthnum_playerswhich specify the strategy used to traverse the game for each player.upd_player=-1means that the graph of all players will be updated. Otherwise, only update the graph ofupd_player.upd_color=[-1]means that all nodes will be updated. Otherwise, only node with the color inupd_colorwill be updated. An example can be found inLiteEFG/baselines/CMD.py. Whentraverse_typeis "de
Related Skills
node-connect
342.5kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
85.3kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
342.5kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
342.5kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
