损失函数
传统损失函数
接下来,让我们来介绍一下统计学和决策理论中的损失函数。损失函数是一个关于真实值和估计值的参数:
损失函数主要用于衡量我们估计的好坏。损失越大,则代表这个估计越差,模型越差。
一个简单而普遍的例子是平方差损失函数。这是一种典型的,与误差的平方成正比的损失函数。具体格式如下:
因为随着估计值的偏离,损失是按照平方增加的,所以平方差损失函数在处理较大误差时,会得到非常糟糕的结果。
因此,在考虑稳健性时,机器学习和统计学一样,会采用另一种损失函数:绝对损失函数。公式如下:
定义一个损失函数最重要的一点就是,该损失函数必须具有稳健性(即它的损失是客观测量的)。因为损失是估计参数和真实参数所组成的误差函数。不管误差是正还是负,都和最终受益相关。如介绍中所说的飓风的例子一样,我们应该找一个和预计参数,估计参数有关,且还和最后飓风到来可能造成的损失有关的具体损失函数。然后,气象学家应该根据这个损失函数做出决策。
把我们的关注重心从更加精确的参数估计转到参数估计所带来的的结果上来,可以使我们更好的优化我们的估计。
比如日常生活中,气象预报员就常常使用损失函数。他们总会夸大降雨的概率,具有目的性的对观众暗示可能有雨。为什么会这样呢?因为天晴带雨伞的损失远远小于下雨没有带雨伞的损失。
期望损失
到目前为止,其实我们一直都在基于一个不太现实的假设。那就是,参数的真正值已知。也就说,只有知道真实值和估计值时,我们才能获得损失的具体值。但是,既然已经知道了这个参数的真实值,那么我们再费心费力的寻找最佳估计干什么呢?其实我们是不知道真实值的,而仅仅是找到了它的可能实现。
在贝叶斯推断中,我们认为未知参数是一个有先验分布和后验分布的实际变量。我们只需要从后验分布中抽取一个值用以表示对真实值的可能实现即可。这样,我们就可以计算与估计相关的损失了。但是当我们有了未知参数的整个后验分布,其实更让我们感兴趣的是整个估计的期望损失。因为,期望损失相比于从整个后验分布中取出一个样本作为损失来说,更加合理。
期望损失可以用到更多的分布函数,并且考虑到错误的对应结果会带来的损失,可以很好的帮助决策。接下来,让我们用一个实例对其进行说明。
实例:“价格竞猜”中的出价优化
“价格竞猜”比赛是之前很热门的一个比赛。比赛规则如下所示:
比赛双方争夺竞猜展台商品的价格。
每位参赛者都能看到独一无二的一套奖品。
观看后,每位参赛者需要给出自己所看到的整套奖品的投标价格总和。
如果投标价格超过实际价格,投标者将会被取消获奖资格。
如果投标价格低于真正的价格,且差距在 $250 以内,则投标者获得两套(我方的和对方的)奖品。
游戏的难度在与平衡价格的不确定性。我们需要出的价格不能太低,也不能过高。
假设,我们记录了之前的 “价格竞猜” 比赛,获得了这些真实价格的先验分布。为了简单起见,这里我们就假设这些奖品的真实价格服从正态分布随机产生,即:
假设我们看到的这套奖品中有两个商品,分别为:
一趟奇妙的加拿大多伦多之旅。
一个可爱的吹雪机。
当然,当我们看到商品使,其实我们对这些奖品的真实价格就有一些猜测,但却也有一定的不确定性。我们可以使用正态分布来表示这种不确定性,即:
为什么我们要使用正态分布来表示我们的对价格取值的信念概率呢?因为,我们可以使用μi参数指定一个公平的价格,并用sigmai表示我们猜测结果的不确定性。则,该套商品的真实价格可以用 Prize1 + Prize2 + ϵ 确定。其中ϵ是一个误差项。
现在,我们具体对两种商品进行估算:
例如,我认为多伦多旅行耳朵的真实价格为 12000,但是有 68% 的概率会下降一个标准差,因此,我设置了上面的分布。现在让我们对真实数据的历史分布、吹风机价格的先验、旅行票价格的先验进行可视化。
import scipy.stats as stats
from IPython.core.pylabtools import figsize
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
figsize(12.5, 9)
norm_pdf = stats.norm.pdf
plt.subplot(311)
x = np.linspace(0, 60000, 200)
sp1 = plt.fill_between(x, 0, norm_pdf(x, 35000, 7500),
color="#348ABD", lw=3, alpha=0.6,
label="historical total prices")
p1 = plt.Rectangle((0, 0), 1, 1, fc=sp1.get_facecolor()[0])
plt.legend([p1], [sp1.get_label()])
plt.subplot(312)
x = np.linspace(0, 10000, 200)
sp2 = plt.fill_between(x, 0, norm_pdf(x, 3000, 500),
color="#A60628", lw=3, alpha=0.6,
label="snowblower price guess")
p2 = plt.Rectangle((0, 0), 1, 1, fc=sp2.get_facecolor()[0])
plt.legend([p2], [sp2.get_label()])
plt.subplot(313)
x = np.linspace(0, 25000, 200)
sp3 = plt.fill_between(x, 0, norm_pdf(x, 12000, 3000),
color="#7A68A6", lw=3, alpha=0.6,
label="Trip price guess")
plt.autoscale(tight=True)
p3 = plt.Rectangle((0, 0), 1, 1, fc=sp3.get_facecolor()[0])
plt.legend([p3], [sp3.get_label()])
知道每个参数的具体分布后,让我们在贝叶斯模型中,将他们定义出来
import pymc3 as pm
# 对两种商品的猜测分布
data_mu = [3e3, 12e3]
data_std = [5e2, 3e3]
# 历史分布
mu_prior = 35e3
std_prior = 75e2
with pm.Model() as model:
true_price = pm.Normal("true_price", mu=mu_prior, sd=std_prior)
prize_1 = pm.Normal("first_prize", mu=data_mu[0], sd=data_std[0])
prize_2 = pm.Normal("second_prize", mu=data_mu[1], sd=data_std[1])
price_estimate = prize_1 + prize_2
# pm.Normal.dist(mu=price_estimate, sd=(3e3)):对price_estimate再取了一次正态分布,得到总价格的估计
# 通过对数损失函数来计算估计与真实的误差
logp = pm.Normal.dist(mu=price_estimate, sd=(3e3)).logp(true_price)
error = pm.Potential("error", logp)
trace = pm.sample(50000, step=pm.Metropolis())
burned_trace = trace[10000:]
# 获得
price_trace = burned_trace["true_price"]
得到样本后,让我们对样本进行可视化,观察整套奖品的价格:
import scipy.stats as stats
figsize(12.5, 4)
x = np.linspace(5000, 40000)
plt.plot(x, stats.norm.pdf(x, 35000, 7500), c="k", lw=2,
label="prior dist. of suite price")
_hist = plt.hist(price_trace, bins=35, normed=True, histtype="stepfilled")
plt.title("Posterior of the true price estimate")
plt.vlines(mu_prior, 0, 1.1*np.max(_hist[0]), label="prior's mean",
linestyles="--")
plt.vlines(price_trace.mean(), 0, 1.1*np.max(_hist[0]),
label="posterior's mean", linestyles="-.")
plt.legend(loc="upper left")
如上图,分别表示了套装价格的先验分布、先验均值和后验均值。
那么,参赛者的损失函数到底应该看起来怎么样呢?下面是一个简单的损失函数的例子:
def showcase_loss(guess, true_price, risk = 80000):
if true_price < guess:
return risk
elif abs(true_price - guess) <= 250:
return -2*np.abs(true_price)
else:
return np.abs(true_price - guess - 250)
risk 表示承受风险。如果 risk 是一个很大的值,表示当你的猜测价格高于真正的价格时是非常糟糕的。如果 risk 的值较小,说明你能够容忍比真实价格高的价格的情况。
如果我们出价低于真实价格且差距小于 250$,那么我们就可以获得两套奖品(这里模拟成一套奖品的两倍)。否则,返回我们我的猜测价格距离可忍耐的最低价格(true_price-250)还差多少。
为了能够以矩阵的形式处理上面的损失函数,现在让我们对上面的损失函数进行重新编写,得到:
# 批量计算损失
def showdown_loss(guess, true_price, risk=80000):
loss = np.zeros_like(true_price)
ix = true_price < guess
loss[~ix] = np.abs(guess - true_price[~ix])
close_mask = [abs(true_price - guess) <= 250]
loss[close_mask] = -2*true_price[close_mask]
loss[ix] = risk
return loss
接下来,让我们看看不同的风险(risk)下的期望损失的变化:
# 假设猜测的
figsize(12.5, 7)
guesses = np.linspace(5000, 50000, 70)
risks = np.linspace(30000, 150000, 6)
# 定义计算损失的函数
def expected_loss(guess, risk): return showdown_loss(
guess, price_trace, risk).mean()
# 设置里这么多个风险系数,计算了不同风险系数下的损害函数
for _p in risks:
results = [expected_loss(_g, _p) for _g in guesses]
plt.plot(guesses, results, label="%d" % _p)
plt.title("Expected loss of different guesses, \nvarious risk-levels of \
overestimating")
plt.legend(loc="upper left", title="Risk parameter")
plt.xlabel("price bid")
plt.ylabel("expected loss")
plt.xlim(5000, 30000)
最大限度的减少损失是明智的选择,也就是说寻找上图中每条曲线的最小值点。更正式的说法是,我们希望对下面的目标函数进行求解,进而减少我们的损失。
期望损失的最小值被叫做贝叶斯行动。我们可以使用 SciPy 的优化程序来求解贝叶斯行动。scipy.optimize 模块中有一个智能搜索函数:fmin。通过该函数可以找出任意单变量或者多变量函数的极值。
接下来,让我们来计算上面损失的最小值点,并将其标注在原图上:
import scipy.optimize as sop
ax = plt.subplot(111)
for _p in risks:
_color = next(ax._get_lines.prop_cycler)
_min_results = sop.fmin(expected_loss, 15000, args=(_p,), disp=False)
_results = [expected_loss(_g, _p) for _g in guesses]
plt.plot(guesses, _results, color=_color['color'])
plt.scatter(_min_results, 0, s=60,
color=_color['color'], label="%d" % _p)
plt.vlines(_min_results, 0, 120000, color=_color['color'], linestyles="--")
print("minimum at risk %d: %.2f" % (_p, _min_results))
plt.title("Expected loss & Bayes actions of different guesses, \n \
various risk-levels of overestimating")
plt.legend(loc="upper left", scatterpoints=1, title="Bayes action at risk:")
plt.xlabel("price guess")
plt.ylabel("expected loss")
plt.xlim(7000, 30000)
plt.ylim(-1000, 80000)
从上图中可以看到,如果风险阈值降低,我们就应该提高我们的出价,使其更能接近真实价格。
当然,你可能会说,我们不用 fmin 函数,也可以从上上个图像中看出这个规律。但是,请注意,在高维空间中,我们是无法用肉眼找到极值的。这个时候,我们就不得不使用 fmin 函数。
当然,有些损失函数的贝叶斯行动是可以用公式表示的。比如,均方差损失。该损失的贝叶斯行动就是后验分布的均值,因此,计算均方差损失的贝叶斯行动是非常快的。这也就是为什么均方差损失函数是贝叶斯推断中最常用的函数。当然,所有的损失都必须根据问题的具体情况进行设定,才是最有效的,例如下面的金融预测的例子。
实例:金融预测
假设现在有一只股票,它在明天的真实回报率为 0.01 。我们建立了一个模型来预测这只股票的未来的价格。而我们将会直接依赖于预测价格对“是否卖出这只股票”进行决策。我们可以通过计算损失,来决定是否卖出这只股票。那么如何定义损失函数呢?让我先来试试平方差损失函数。
假设现在现在有两个回报预测值 -0.01 和 0.03。现在,让我们来计算一下它们的损失:
我们可以很清楚的知道,这两个预测值的平方差损失是一样的。但是事实上,这两个预测值造成的结果大不一样。假设我们根据预测值进行决策,如果我们预测明天是 0.03,那么我们就会保留这只股票,等到明天再来卖。而明天的真实回报率是 0.03,可以说我们确实赚到了钱。但如果是 -0.01,那么我们就会在今天把它卖掉,而明天的真实回报率为正的,那么我们今天买,就是亏本的。而平方差损失无法体现这一点,因此不适合用于该实例中。
因此,我们需要一个更好的损失函数,既考虑了预测价格的正负号和真正所获利润的损失函数。
在定义损失函数之前,我们需要了解一下金融的基本常识与定义。
上行风险:预测的方向错误(即本来是负的却预测成了正的)。
下行风险:预测的方向错误(即本来是正的却预测成了负的)。
量级:预测值和真实值的差距。
损失函数如下:
def stock_loss(true_return, yhat, alpha=100.):
if true_return * yhat < 0:
# 符号相反的情况
return alpha*yhat**2 - np.sign(true_return)*yhat \
+ abs(true_return)
else:
return abs(true_return - yhat)
# 传入参数分别为:真实回报,预测回报,风险系数
stock_loss(true_return=0.1, yhat=0.3, alpha=100), stock_loss(
true_return=0.1, yhat=-0.1, alpha=100)
从上面测试代码中我们可以很明显的看到,通过该损失函数可以很明显的比较出预测值为 -0.01 和 0.03 的差别,且预测为 0.03 的损失远小于预测为 -0.1 的损失。
接下来,让我们画出两种不同方向下,真实损失与预测值的关系图:
figsize(12.5, 4)
# 设置第一个真实回报为0.05
true_value = .05
pred = np.linspace(-.04, .12, 75)
plt.plot(pred, [stock_loss(true_value, _p) for _p in pred],
label="Loss associated with\n prediction if true value = 0.05", lw=3)
plt.vlines(0, 0, .25, linestyles="--")
plt.xlabel("prediction")
plt.ylabel("loss")
plt.xlim(-0.04, .12)
plt.ylim(0, 0.25)
# 设置第二个真实回报为-0.02
true_value = -.02
plt.plot(pred, [stock_loss(true_value, _p) for _p in pred], alpha=0.6,
label="Loss associated with\n prediction if true value = -0.02", lw=3)
plt.legend()
plt.title("Stock returns loss if true value = 0.05, -0.02")
其中:蓝色的线表示当真实回报率为 0.05 时,预测值与损失的关系图像。橙黄色的线表示当真实回报率为 -0.02 时,预测值与损失的关系图像。
从上面可以看出,无论是量级很大的下行风险还是量级很大的上级风险都会存在大量的损失。但是如果预测值和真实值的方向正确,那么所造成的极端损失会小于方向相反的。换句话说,预测值和真实值的符号相同时,损失函数的斜率会比相反时更小,更平稳一些。
股票数据的模拟
股票的预测最终的目的是希望产生效益。我们通过模型预测出了明天的交易信号(明天的回报预测值)。然后根据这个信号来计算,如果此时卖出股票所能够得到的回报。
因此,我们需要交易信号和真实回报的历史数据。为了简单起见,这里我们自己来模拟这些数据,如下:
N = 100
X = 0.025*np.random.randn(N)
Y = 0.5*X + 0.01*np.random.randn(N)
# 画出模拟数据的最小二乘线
ls_coef_ = np.cov(X, Y)[0, 1]/np.var(X)
ls_intercept = Y.mean() - ls_coef_*X.mean()
plt.scatter(X, Y, c="k")
plt.xlabel("trading signal")
plt.ylabel("returns")
plt.title("Empirical returns vs trading signal")
plt.plot(X, ls_coef_*X + ls_intercept, label="Least-squares line")
plt.xlim(X.min(), X.max())
plt.ylim(Y.min(), Y.max())
plt.legend(loc="upper left")
上图的横坐标表示交易信号,纵坐标表示回报。那条线是最小二乘线,是利用最小二乘法拟合数据得到的,用来描述数据集的直线。
股票的预测
但是,从上图可以看到,最小二乘法没有随机性,只是一条光秃秃的直线。现在,让我们使用贝叶斯来重新对数据进行模拟。我们假设模型如下:
接下来,我们就需要利用贝叶斯推断,结合上面的数据集,得到这些参数的后验分布,并找到这些分布中最佳的值。
我们还是假设α、β 的先验是正态分布,σ 为 0 到 100 的均匀分布。定义贝叶斯模型的代码如下
import pymc3 as pm
with pm.Model() as model:
std = pm.Uniform("std", 0, 100)
beta = pm.Normal("beta", mu=0, sd=100)
alpha = pm.Normal("alpha", mu=0, sd=100)
mean = pm.Deterministic("mean", alpha + beta*X)
obs = pm.Normal("obs", mu=mean, sd=std, observed=Y)
trace = pm.sample(10000, step=pm.Metropolis())
burned_trace = trace[2000:]
让我们首先把之前说的金融行业的损失函数进行改编,改编成可以接收一个矩阵的新损失函数(方便处理):
def stock_loss(price, pred, coef=500):
"""vectorized for numpy"""
sol = np.zeros_like(price)
ix = price*pred < 0
sol[ix] = coef*pred**2 - np.sign(price[ix])*pred + abs(price[ix])
sol[~ix] = abs(price[~ix] - pred)
return sol
接下来,我们就需要使用之前说到的 fmin 函数来寻找整个损失的极小值点,即最佳点。 并将最佳点(最佳参数)带入原来的模型中,并对模型进行可视化操作。代码如下:
from scipy.optimize import fmin
figsize(12.5, 6)
std_samples = burned_trace["std"]
alpha_samples = burned_trace["alpha"]
beta_samples = burned_trace["beta"]
N = std_samples.shape[0]
noise = std_samples*np.random.randn(N)
def possible_outcomes(signal): return alpha_samples + \
beta_samples*signal + noise
opt_predictions = np.zeros(50)
trading_signals = np.linspace(X.min(), X.max(), 50)
for i, _signal in enumerate(trading_signals):
_possible_outcomes = possible_outcomes(_signal)
def tomin(pred): return stock_loss(_possible_outcomes, pred).mean()
# 得到最佳预测
opt_predictions[i] = fmin(tomin, 0, disp=False)
plt.xlabel("trading signal")
plt.ylabel("prediction")
plt.title("Least-squares prediction vs. Bayes action prediction")
plt.plot(X, ls_coef_*X + ls_intercept, label="Least-squares prediction")
plt.xlim(X.min(), X.max())
plt.plot(trading_signals, opt_predictions, label="Bayes action prediction")
plt.legend(loc="upper left")
其中蓝色的线表示利用最小二乘法所进行的预测。橙色的先表示利用贝叶斯推断,再利用fmin寻找最佳参数,进而做出的预测。
从上图可以看出,当信号越来越极端时,我们对正负回报越来越自信(即对明天是降还是升,更加自信),进而使曲线收敛于最小二乘线。
可以看出,针对于贝叶斯回归线,在交易信号仅仅为 0 时,回报也都趋近于 0 (也就是较为平缓)。这是因为,我们预测的值为 -0.001 时,我们的预测值其实和正方向的值相差不大,我们没有多大信心去说此时应该卖掉股票还是不买,因此损失也趋近于 0 。而当我们的预测为 - 0.04,这种回收率就会正方向相差很大,我们就可以很自信的确定明天肯定是降(因为即时有 0.02 的预测误差,我们的预测值加上 0.02 也是负的)。这个特性,有个名字叫做稀疏预测(即离 0 越远,损失越大,越对自己预测出来的正负值有自信)。这也就是为什么,在此情况下,贝叶斯回归线比最小二乘更优的原因。
稀疏预测模型(即贝叶斯回归模型)不是千方百计的去拟合数据,它做的其实是利用我们定义的股票损失来寻找最低损失,为我们做出最佳决策。而最小二乘法只是机械地去找到平方误差下的最佳拟合。