机器学习(四):商品期货多种回归算法的运用和比较

Author: ianzeng123, Created: 2023-12-08 18:48:38, Updated: 2024-02-28 21:47:26

img

哈喽,小伙伴们!今天我们来聊聊一个在商品期货量化中非常有趣的东西——回归算法!

首先,我们来想象一下,如果我们想要预测商品期货的价格,应该怎么做呢?可能有些人会想,看看最近的价格走势不就行了嘛!但是,这样真的准确吗?答案是——不够准确!因为商品期货价格受到很多因素的影响,比如市场供需、宏观经济指标、政策因素等等。这些因素都会对价格产生影响,所以我们需要一种方法来找出这些因素之间的关系,从而更准确地预测价格。

这时候,回归算法就派上用场啦!它可以帮助我们找出一组数据之间的关系。比如,我们可以把历史数据喂给回归算法,让它自己去找出商品期货价格和那些影响因素之间的关系。这样,我们就可以得到一个模型,用来预测未来的商品期货价格啦!

而且呢,回归算法还可以帮助我们优化投资策略哦!我们可以使用回归算法来分析不同的投资组合和投资收益之间的关系,然后优化我们的投资组合,让投资收益最大化!

总的来说,回归算法就像是一个超级聪明的侦探,能够帮助我们找出数据之间的秘密关系,让我们更好地理解市场、提高预测能力、优化投资策略。是不是很神奇呢?

最小二乘法

首先,我们来认识一下最小二乘法。这个方法的基本思想特别简单,就是通过最小化误差的平方和来拟合一个线性模型。就像我们有一堆数据点,我们希望找到一条直线,让这条直线尽可能地接近这些数据点。

我们定义了一个训练数据序列,用 X 来预测 Y。然后使用最小二乘法来定义了一个最小化问题。通过解这个最小化问题,我们就可以得到拟合数据的直线啦!

以下是一个简单的最小二乘回归的实现,它使用numpy库进行计算:

import numpy as np

class LeastSquaresRegression:
    
    def __init__(self, num_features: int):
        self.n = num_features
        self.w = np.zeros(self.n)
        
    def fit(self, X: np.ndarray, y: np.ndarray) -> None:
        # 添加偏置项
        X = np.c_[X, np.ones((X.shape[0], 1))]
        
        # 计算权重向量w
        self.w = np.linalg.inv(X.T @ X) @ X.T @ y
        
    def predict(self, x: np.ndarray) -> float:
        # 添加偏置项
        x = np.c_[x, np.ones((1, 1))]
        # 计算预测值
        return x @ self.w

这里定义了一个名为LeastSquaresRegression的类,它有一个初始化函数和一个fit方法来拟合模型,以及一个predict方法来预测新数据的标签。在fit方法中,我们首先添加了一个偏置项,然后使用numpy的线性代数函数计算权重向量w。在predict方法中,我们同样添加了偏置项,并使用计算出的权重向量进行预测。

接下来我们使用一份数据来验证下这种方法的有效性,这份数据是多因子模型的数据,包含自变量’RollOver’,‘Std’,‘Skew’,‘Kurt’, ‘br_63’, ‘wr_5’, ‘PriceMom’,‘VolumeMom’和因变量’Returns’。

RollOver	Std	Skew	Kurt	br_63	wr_5	PriceMom	VolumeMom	Returns
0	-0.008327	0.000195	-0.026764	0.030578	-0.052012	0.013926	-0.034208	0.038888	0.012024
1	0.007128	0.023512	0.009441	-0.008438	-0.009726	0.014336	-0.019629	-0.027252	0.023680
2	-0.017382	0.020795	-0.020766	0.023484	-0.003485	-0.002345	-0.030002	0.037297	0.009059
3	0.006700	-0.021326	-0.021227	0.027277	-0.052713	-0.046430	-0.018186	0.016547	0.016382
4	0.005480	-0.002760	-0.023195	0.018602	0.006989	-0.008384	-0.042303	0.011396	0.034518

然后我们进行下面的工作:

  • 数据准备:

从 DataFrame (prodata) 中提取特征矩阵 X 和目标变量 y。 删除包含 NaN 值的行。

  • 数据划分:

使用 train_test_split 函数将数据集分为训练集 (X_train, y_train) 和测试集 (X_test, y_test),其中测试集占总数据的 20%,random_state 参数设置为 42 以确保划分的随机性可复现。 模型初始化:

创建了一个 LeastSquaresRegression 模型,传入特征的数量。

  • 模型训练:

使用训练集 (X_train, y_train) 对 LeastSquaresRegression 模型进行训练。调用 fit 方法,拟合模型并计算权重向量。

  • 模型预测:

使用训练好的模型对测试集 (X_test) 进行预测,得到 predictions 列表。

  • 准确率评估:

计算模型在测试集上的准确率。通过比较模型预测的符号与实际目标变量的符号,统计了正确预测的样本数。最后,计算了准确率并将结果以百分比形式打印出来。

# Extract features and target variable
X = prodata.iloc[:, :-1].values  # Features
y = prodata.iloc[:, -1].values    # Target variable

# Split the data into training and testing sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Initialize the LeastSquaresRegression model
num_features = X_train.shape[1]
model = LeastSquaresRegression(num_features)

# Train the model on the training set
model.fit(X_train, y_train)

# Make predictions on the test set
predictions = [model.predict(x) for x in X_test]

# Evaluate accuracy
correct_predictions = np.sum(np.sign(predictions) == np.sign(y_test))
total_samples = len(y_test)
accuracy = correct_predictions / total_samples

print(f"Accuracy: {accuracy * 100:.2f}%")

最后的结果预测正确率是58.72%,我们可以和下面不同的回归方法比较一下。

💡小提示: 在处理新数据点时,你无需重新计算最小二乘模型,只需更新拟合的系数。这样可以节省计算时间并更加强调最近的数据哦!

L2正则化

接下来,我们来聊聊 L2 正则化。这个方法特别有用,可以防止我们的模型过拟合。过拟合是什么意思呢?就是我们的模型在训练数据上表现得太好了,以至于把训练数据的一些噪声也当作了有用的信息。这样就会导致模型在测试数据上的表现变差。

L2 正则化通过在最小二乘问题中添加一个正则化项,可以防止过拟合。就像给模型加了一个惩罚项,让模型不要过于复杂。

这是一个L2回归(L2 Regression)的类实现。下面是对这段代码的逐行注释以及简单的介绍:

# 导入numpy库,numpy是Python中用于科学计算的库,提供了大量的数学函数和工具
import numpy as np

# 定义一个名为L2Regression的类
class L2Regression:
    
    # 定义类的初始化函数,接收两个参数:num_features表示特征的数量,lam表示正则化参数
    def __init__(self, num_features: int, lam: float):
        # 保存特征的数量
        self.n = num_features
        # 保存正则化参数
        self.lam = lam
        # 初始化权重向量为0,长度为特征的数量
        self.w = np.zeros(self.n)
        # 初始化协方差矩阵,对角线元素为正则化参数乘以1,其余元素为0
        self.P = np.diag(np.ones(self.n) * self.lam)
        
    # 定义更新函数,接收两个参数:x表示一个数据点的特征向量,y表示对应的标量目标值
    def update(self, x: np.ndarray, y: float) -> None:
        # 计算r,其值为1加上x的转置与协方差矩阵P和x的乘积的和
        r = 1 + (x.T @ self.P @ x)
        # 计算k,它是协方差矩阵P和x的乘积除以r的结果
        k = (self.P @ x) / r
        # 计算e,它是y减去x和权重向量w的内积的结果
        e = y - x @ self.w
        # 更新权重向量w,使其加上k和e的乘积
        self.w = self.w + k * e
        # 将k的形状改变为(-1, 1),以便下一行代码可以使用它
        k = k.reshape(-1, 1)
        # 更新协方差矩阵P,使其减去k和k的转置的乘积乘以r的结果
        self.P = self.P - (k @ k.T) * r
        
    # 定义预测函数,接收一个参数:x表示一个数据点的特征向量,返回一个标量预测值
    def predict(self, x: np.ndarray) -> float:
        # 返回权重向量w和特征向量x的内积的结果,即预测值
        return self.w @ x

这个类实现了一个简单的L2回归模型。在初始化时,它接收特征数量和一个正则化参数。在每次更新时,它使用梯度下降算法来更新权重向量,并更新协方差矩阵。预测时,它使用当前的权重向量来预测新的数据点的目标值。

# Define a list of lambda values to try
lam_values = [0.001, 0.01, 0.1, 1, 10]

best_accuracy = 0
best_lam = 0

# Iterate over different lambda values
for lam in lam_values:
    # Initialize the L2Regression model
    model = L2Regression(X_train.shape[1], lam)

    # Train the model on the training set
    for i in range(len(X_train)):
        model.update(X_train[i], y_train[i])

    # Make predictions on the test set
    predictions = [model.predict(x) for x in X_test]

    # Evaluate accuracy
    correct_predictions = np.sum(np.sign(predictions) == np.sign(y_test))
    total_samples = len(y_test)
    accuracy = correct_predictions / total_samples

    # Print the accuracy for the current lambda value
    print(f"Lambda: {lam}, Accuracy: {accuracy * 100:.2f}%")

    # Update the best accuracy and lambda if needed
    if accuracy > best_accuracy:
        best_accuracy = accuracy
        best_lam = lam

# Print the best lambda and corresponding accuracy
print(f"Best Lambda: {best_lam}, Best Accuracy: {best_accuracy * 100:.2f}%")

使用同样的测试集数据,lam参数定义为0.001是,正确率是58.88%。

💡小提示: L2 正则化不仅可以防止过拟合,还可以让我们的模型更加健壮哦!

指数加权法

现在我们来了解一下指数加权法。这个方法通过对最近的数据赋予更高的权重,对更旧的数据赋予更低的权重。这样可以让我们的模型更加关注最近的数据,对市场的变化做出更快的反应。

这是一个指数衰减L2回归(Exponential Decay L2 Regression)的类实现。与之前的L2回归相比,这个版本的权重更新考虑了指数衰减(或称为半衰期衰减)。下面是对这段代码的逐行注释以及简单的介绍:

# 导入numpy库
import numpy as np

# 定义一个名为ExpL2Regression的类
class ExpL2Regression:
    
    # 定义类的初始化函数,接收三个参数:num_features表示特征的数量,lam表示正则化参数,halflife表示半衰期
    def __init__(self, num_features: int, lam: float, halflife: float):
        # 保存特征的数量
        self.n = num_features
        # 保存正则化参数
        self.lam = lam
        # 计算beta值,这是基于给定的半衰期来计算的衰减率
        self.beta = np.exp(np.log(0.5) / halflife)
        # 初始化权重向量为0,长度为特征的数量
        self.w = np.zeros(self.n)
        # 初始化协方差矩阵,对角线元素为正则化参数乘以1,其余元素为0
        self.P = np.diag(np.ones(self.n) * self.lam)
        
    # 定义更新函数,接收两个参数:x表示一个数据点的特征向量,y表示对应的标量目标值
    def update(self, x: np.ndarray, y: float) -> None:
        # 计算r,但这次考虑了指数衰减的beta值
        r = 1 + (x.T @ self.P @ x) / self.beta
        # 计算k,与之前的L2回归相似,但这次也考虑了beta值
        k = (self.P @ x) / (r * self.beta)
        # 计算e,它是y减去x和权重向量w的内积的结果
        e = y - x @ self.w
        # 更新权重向量w,使其加上k和e的乘积
        self.w = self.w + k * e
        # 将k的形状改变为(-1, 1),以便下一行代码可以使用它
        k = k.reshape(-1, 1)
        # 更新协方差矩阵P,这次也考虑了指数衰减的beta值
        self.P = self.P / self.beta - (k @ k.T) * r
        
    # 定义预测函数,与之前的L2回归相同
    def predict(self, x: np.ndarray) -> float:
        # 返回权重向量w和特征向量x的内积的结果,即预测值
        return self.w @ x

这个类实现了一个带有指数衰减的L2回归模型。在初始化时,除了特征数量和正则化参数外,还需要指定一个半衰期参数。这个半衰期参数决定了权重更新的衰减速度。在每次更新时,权重和协方差矩阵都会根据新的数据点进行调整,并考虑指数衰减的效果。预测时,它使用当前的权重向量来预测新的数据点的目标值。

best_accuracy = 0
best_params = {}


lam_range = np.arange(0, 2, 0.1)  # 在10的指数范围内取7个值,从10^-4到10^2
halflife_range = [1,2,4,8]  # 在5到50的范围内取步长为5的值


# 遍历参数范围
for lam in lam_range:
    for halflife in halflife_range:
        # 初始化 ExpL2Regression 模型
        model = ExpL2Regression(X_train.shape[1], lam, halflife)

        # 训练模型
        for i in range(len(X_train)):
            model.update(X_train[i], y_train[i])

        # 预测测试集
        predictions = [model.predict(x) for x in X_test]

        # 评估准确率
        correct_predictions = np.sum(np.sign(predictions) == np.sign(y_test))
        total_samples = len(y_test)
        accuracy = correct_predictions / total_samples

        # 如果当前参数组合的准确率更高,则更新最佳参数
        if accuracy > best_accuracy:
            best_accuracy = accuracy
            best_params = {'lam': lam, 'halflife': halflife}

# 打印最佳参数和对应的准确率
print(f"Best Parameters: {best_params}")
print(f"Best Accuracy: {best_accuracy * 100:.2f}%")

使用 {‘lam’: 0.1, ‘halflife’: 8},获得了最好的收益率57.01%。

💡小提示: 指数加权法特别适合处理时间序列数据哦!

L1正则化

最后,我们来介绍一下 L1 正则化。这个方法与 L2 正则化类似,但是通过引入一个额外的项来实现更新。L1 正则化通常用于特征选择,它可以让模型的某些特征权重为0,从而达到简化模型的效果。

这段代码定义了一个名为ExpL1L2Regression的类,它是一种结合了L1和L2正则化的回归模型。这种回归模型通常用于防止过拟合,通过在损失函数中添加正则化项来惩罚模型的复杂性。

下面是对这段代码的逐行注释以及简单的介绍:

# 导入numpy库
import numpy as np

# 定义一个名为ExpL1L2Regression的类
class ExpL1L2Regression:
    
    # 定义类的初始化函数,接收五个参数:num_features表示特征的数量,lam表示正则化参数,halflife表示半衰期,gamma表示一个额外的参数,epsilon表示一个小的数值,用于防止除以0的情况
    def __init__(self, num_features: int, lam: float, halflife: float, gamma: float, epsilon: float):
        # 保存特征的数量
        self.n = num_features
        # 保存正则化参数
        self.lam = lam
        # 保存另一个参数gamma
        self.gamma = gamma
        # 保存epsilon值,用于防止除以0的情况
        self.epsilon = epsilon
        # 计算beta值,这是基于给定的半衰期来计算的衰减率
        self.beta = np.exp(np.log(0.5) / halflife)
        # 初始化权重向量为0,长度为特征的数量
        self.w = np.zeros(self.n)
        # 初始化协方差矩阵,对角线元素为正则化参数乘以1,其余元素为0
        self.P = np.diag(np.ones(self.n) * self.lam)
        
    # 定义更新函数,接收两个参数:x表示一个数据点的特征向量,y表示对应的标量目标值
    def update(self, x: np.ndarray, y: float) -> None:
        # 计算r,但这次考虑了指数衰减的beta值
        r = 1 + (x.T @ self.P @ x) / self.beta
        # 计算k,与之前的L2回归相似,但这次也考虑了beta值
        k = (self.P @ x) / (r * self.beta)
        # 计算e,它是y减去x和权重向量w的内积的结果
        e = y - x @ self.w
        
        # 将k的形状改变为(-1, 1),以便下一行代码可以使用它
        k = k.reshape(-1, 1)
        # 更新协方差矩阵P,这次也考虑了指数衰减的beta值
        self.P = self.P / self.beta - (k @ k.T) * r
        
        extra = (
            # 这部分使用了L1正则化的项,gamma是一个额外的参数,用于控制L1正则化的强度
            self.gamma * ((self.beta - 1) / self.beta) *
            # 从协方差矩阵P中减去k @ w的乘积后再与原来的P进行矩阵乘法运算,最后再将结果乘以(sign(w) / (abs(w) + epsilon)),这个操作与Lasso回归中的正则化项相似。
            (np.eye(self.n) - k @ self.w.reshape(1, -1)) @ self.P @ (np.sign(self.w) / (np.abs(self.w) + self.epsilon))
        )
        
        # 更新权重向量w,使其加上k和e的乘积以及extra的结果
        self.w = self.w + k.flatten() * e + extra
        
    # 定义预测函数,与之前的L2回归相同,返回权重向量w和特征向量x的内积的结果作为预测值。          
    def predict(self, x: np.ndarray) -> float:
        return self.w @ x

这个类实现了一个带有指数衰减的L1和L2混合正则化的回归模型。在初始化时,除了特征数量和正则化参数外,还需要指定两个额外的参数:gamma用于控制L1正则化的强度,epsilon是一个小数值,用于防止除以0的情况。在每次更新时,它首先使用与之前相似的步骤更新协方差矩阵P,然后使用一个额外的操作来添加L1正则化项。这个额外的操作类似于Lasso回归中的正则化项。最后,它更新权重向量w以包括新的数据点的影响以及L1正则化的项进行预测。

# 初始化 ExpL1L2Regression 模型
num_features = X_train.shape[1]
lam_range = np.logspace(-4, 2, 7)  # 在10的指数范围内取7个值,从10^-4到10^2
halflife_range = np.arange(5, 50, 5)  # 在5到50的范围内取步长为5的值
gamma_range = np.logspace(-4, 1, 6)  # 在10的指数范围内取6个值,从10^-4到10^1
epsilon_range = np.logspace(-8, -4, 5)  # 在10的指数范围内取5个值,从10^-8到10^-4

model = ExpL1L2Regression(num_features, lam, halflife, gamma, epsilon)

best_accuracy = 0
best_params = {}

# 遍历参数范围
for lam in lam_range:
    for halflife in halflife_range:
        for gamma in gamma_range:
            for epsilon in epsilon_range:
                # 初始化 ExpL1L2Regression 模型
                model = ExpL1L2Regression(X_train.shape[1], lam, halflife, gamma, epsilon)

                # 训练模型
                for i in range(len(X_train)):
                    model.update(X_train[i], y_train[i])

                # 预测测试集
                predictions = [model.predict(x) for x in X_test]

                # 评估准确率
                correct_predictions = np.sum(np.sign(predictions) == np.sign(y_test))
                total_samples = len(y_test)
                accuracy = correct_predictions / total_samples

                # 如果当前参数组合的准确率更高,则更新最佳参数
                if accuracy > best_accuracy:
                    best_accuracy = accuracy
                    best_params = {'lam': lam, 'halflife': halflife, 'gamma': gamma, 'epsilon': epsilon}

# 打印最佳参数和对应的准确率
print(f"Best Parameters: {best_params}")
print(f"Best Accuracy: {best_accuracy * 100:.2f}%")

当参数为{‘lam’: 0.001, ‘halflife’: 30, ‘gamma’: 10.0, ‘epsilon’: 1e-05},获得了最后的预测正确率58.57%。

💡小提示: L1 正则化可以让我们的模型更加稀疏,更容易解释哦!

结合使用

当然啦,这些方法并不是孤立的,我们可以根据实际问题的需要将它们结合起来使用。比如我们可以将 L1 和 L2 正则化结合起来使用,这样既可以防止过拟合又可以简化模型。我们还可以将指数加权法与 L2 正则化结合起来使用,让模型更加关注最近的数据的同时防止过拟合。总之,这些方法提供了灵活的方式来适应不同的数据和问题,我们可以根据实际情况选择适当的方法以更好地应对特定的数据动态性哦!

本系列课程旨在为大家介绍机器学习技术在商品期货量化交易中的应用,其他相关文章请点击下面链接:


更多内容