多次听说或看到抽样平均方法 Sample Average Approximation ,但不是真正理解这个方法到底怎么用的。决定写一篇博客,将每次看到的心得体会记录下来。
对于一个随机规划问题,
min f ( x , ξ ) \min\quad f(x, \xi) minf(x,ξ)
其中 ξ \xi ξ 为一个随机变量,而 x x x 是一个求解变量。抽样平均方法 SAA 的大致含义是用抽样的方法将随机变量用样本表示,从而将随机规划问题转化为确定性问题。 假设随机变量 ξ \xi ξ 的样本分别为: ξ 1 , ξ 2 , … , ξ N \xi_1, \xi_2,\dots, \xi_N ξ1,ξ2,…,ξN. 则原问题转化为:
min 1 N ∑ i = 1 N f ( x , ξ i ) \min\quad \frac{1}{N}\sum_{i=1}^{N} f(x, \xi_i) minN1i=1∑Nf(x,ξi)
而在每个样本下, min f ( x , ξ ) \min f(x, \xi) minf(x,ξ) 可以通过线性规划求解。
该方法有一定的适用范围,这一点还没弄清楚。
1. 单阶段时
例如一个单阶段报童模型:
max π = p min { Q , d } − c Q + γ max { Q − d , 0 } \max\quad\pi= p\min\{Q, d\}-cQ+\gamma\max\{Q-d, 0\} maxπ=pmin{Q,d}−cQ+γmax{Q−d,0}
假如随机需求抽样了
N
N
N 个样本,每个样本值
d
i
d^i
di,则表达式可以转化为:
max
π
=
1
N
∑
i
=
1
N
p
min
{
Q
,
d
i
}
−
c
Q
+
γ
max
{
Q
−
d
i
,
0
}
\max\quad\pi= \frac{1}{N}\sum_{i=1}^Np\min\{Q, d^i\}-cQ+\gamma\max\{Q-d^i, 0\}
maxπ=N1i=1∑Npmin{Q,di}−cQ+γmax{Q−di,0}
进一步转化为可以用规划软件求解的混合整数规划模型,首先引入两个辅助变量表示目标函数中的 min ,max 表达式:
max π = 1 N ∑ i = 1 N ( p x i + γ y i ) − c Q s . t . ∀ i = 1 , 2 , … , N x i = min { Q , d i } y i = max { Q − d i , 0 } Q ≥ 0 \begin{aligned} \max\quad&\pi= \frac{1}{N}\sum_{i=1}^N(px^i+\gamma y^i)-cQ\\ s.t.\quad&\forall i=1, 2, \dots, N\\ &x^i=\min\{Q, d^i\}\\ &y^i=\max\{Q-d^i, 0\}\\ & Q\geq 0 \end{aligned} maxs.t.π=N1i=1∑N(pxi+γyi)−cQ∀i=1,2,…,Nxi=min{Q,di}yi=max{Q−di,0}Q≥0
可以再进一步引入辅助变量 δ \delta δ 将约束条件中的 min 或 max 转化为线性表达式,不过很多规划软件已经可以求解了。
使用 python 调用 gurobi 实现单阶段报童模型的例子:
# -*- coding: utf-8 -*-
"""
Created on Tue Feb 23 15:08:06 2021
@author: zhen chen
MIT Licence.
Python version: 3.7
Description: SAA for single period newsvendor
"""
import numpy as np
import scipy.stats as st
from gurobipy import *
# 使用拉丁超立方抽样生成样本
def generate_sample(sample_num, trunQuantile, mu):
samples = [0 for i in range(sample_num)]
for i in range(sample_num):
rand_p = np.random.uniform(trunQuantile*i/sample_num, trunQuantile*(i+1)/sample_num)
samples[i] = st.poisson.ppf(rand_p, mu)
return samples
price = 6
vari_cost = 2
sal_value = 1
mean_demands = 5
sample_num = 10
trunQuantile = 0.999
samples = generate_sample(sample_num, trunQuantile, mean_demands)
try:
# Create a new model
m = Model("saa-mip")
# Create variables
Q = m.addVar(vtype = GRB.CONTINUOUS) # only one value for all scenarios
realized_demand = [m.addVar(vtype = GRB.CONTINUOUS) for s in range(sample_num)] # auxiliary variable
remnant_demand = [m.addVar(vtype = GRB.CONTINUOUS) for s in range(sample_num)] # auxiliary variable
profit = LinExpr()
profit = price * sum(realized_demand) / sample_num - vari_cost * Q + sal_value * sum(remnant_demand) / sample_num
m.update()
m.setObjective(profit, GRB.MAXIMIZE)
# Add constraints
for s in range(sample_num):
m.addGenConstrMin(realized_demand[s], [Q], samples[s])
m.addConstr(remnant_demand[s] == Q - realized_demand[s])
m.update()
m.optimize()
print('ordering quantity in the first period is: %.2f ' % Q.X)
except GurobiError as e:
print('Error code ' + str(e.errno) + ": " + str(e))
except AttributeError:
print('Encountered an attribute error')
基本上只需 10 个需求样本就能算出正确的最优订货量 7.
2. 多阶段时
多阶段的基本思路跟单阶段一样,只是每个阶段都要抽取 N N N 个样本,所以 T T T 个阶段会有 N T N^T NT 个样本,运算复杂度为指数级。此时,可能会涉及到怎么减少样本量,同时又不那么影响计算结果的处理技巧。
我试了试多阶段的报童模型,最优期望利润一般比随机动态规划的计算结果大一些。每个阶段的样本数比较小时,计算结果波动较大。若阶段数目增加到 4 个阶段以上,每个阶段样本数为 10,SAA 的计算速度实在太慢了(实践觉得,只要总样本数小于 10 万,gurobi 求解 SAA 还是挺快的)。
多阶段时,SAA 求解结果的上界下界不太好确定,只能求到一个下界(最小化问题时),对于上界,要想方设法得到问题的一个可行解,我看目前的论文中也没有明确。