• 人工智能底层自行实现篇3——逻辑回归(下)


    3. 代码实现


    接上一篇文章

    3.1 问题背景

    应用案例背景

    假设我们有一个数据集,包含患者的多种生理指标(如年龄、性别、体重指数(BMI)、血糖水平等)以及他们是否被诊断为糖尿病(是或否)。我们的目标是建立一个逻辑回归模型,根据这些生理指标预测一个患者是否有糖尿病的概率。

    数据集表示

    假设我们的数据集包含以下特征:

    • X 1 X_1 X1: 年龄
    • X 2 X_2 X2: 性别(0表示女性,1表示男性)
    • X 3 X_3 X3: 体重指数(BMI)
    • X 4 X_4 X4: 血糖水平
    • Y Y Y: 是否有糖尿病(0表示没有,1表示有)
    逻辑回归模型

    我们将使用逻辑回归模型来预测 P ( Y = 1 ∣ X ) P(Y=1|X) P(Y=1∣X),即给定生理指标X时,患者有糖尿病的概率。模型的形式为:

    P ( Y = 1 ∣ X ) = 1 1 + e − ( β 0 + β 1 X 1 + β 2 X 2 + β 3 X 3 + β 4 X 4 ) P(Y=1|X) = \frac{1}{1 + e^{-(\beta_0 + \beta_1X_1 + \beta_2X_2 + \beta_3X_3 + \beta_4X_4)}} P(Y=1∣X)=1+e(β0+β1X1+β2X2+β3X3+β4X4)1

    3.2 数据准备

    # 模拟创建数据
    import pandas as pd
    import numpy as np
    # 设置随机种子以保证结果可复现
    np.random.seed(0)
    # 创建数据
    data_size = 1000  # 数据集大小
    # 年龄范围从20到80
    ages = np.random.randint(20, 80, size=data_size)
    # 性别为0或1
    genders = np.random.randint(0, 2, size=data_size)
    # BMI范围从18到35
    bmis = np.random.uniform(18, 35, size=data_size)
    # 血糖水平范围从70到140
    glucose_levels = np.random.uniform(70, 140, size=data_size)
    # 指定结果:是否有糖尿病
    labels = np.random.randint(0, 2, size=data_size)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    # 定义结果变量——先假定一个模型来生成数据的结果
    # 使用逻辑函数生成糖尿病的概率,并依此生成0或1的标签
    beta = np.array([-8, 0.05, 0.25, 0.1, 0.05])  # 假定的模型参数
    X = np.column_stack((np.ones(data_size), ages, genders, bmis, glucose_levels))
    linear_combination = X.dot(beta)
    probabilities = 1 / (1 + np.exp(-linear_combination))
    labels = np.random.binomial(1, probabilities)
    
    # 创建DataFrame
    df = pd.DataFrame({
        'Age': ages,
        'Gender': genders,
        'BMI': bmis,
        'Glucose_Level': glucose_levels,
        'Diabetes': labels
    })
    
    # 显示前几行数据
    print(df.head())
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
       Age  Gender        BMI  Glucose_Level  Diabetes
    0   64       1  21.150926      74.332242         1
    1   67       0  34.928860     127.491243         1
    2   73       0  20.199048      96.576651         1
    3   20       1  26.014774     110.008514         1
    4   23       1  19.157583     138.848879         1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3.3 数据探索

    # 数据集中有多少患者被诊断为糖尿病
    print(df['Diabetes'].value_counts())
    
    • 1
    • 2
    1    880
    0    120
    Name: Diabetes, dtype: int64
    
    • 1
    • 2
    • 3

    3.4 手动实现逻辑回归

    逻辑回归模型的训练过程就是最大化似然函数的过程。我们可以使用梯度下降法来最大化似然函数。下面我们将手动实现逻辑回归模型的训练过程。

    使用梯度下降来最大化逻辑回归的似然函数,实际上我们通常是在最小化对数似然函数的负值,这被称为对数损失或交叉熵损失。这种方法称为梯度下降,其目标是找到一组参数(权重和偏置),使得损失函数最小化。
    公式为:
    J ( w , b ) = − ∑ i = 1 n [ y ( i ) log ⁡ ( σ ( z ( i ) ) ) + ( 1 − y ( i ) ) log ⁡ ( 1 − σ ( z ( i ) ) ) ] J(\mathbf{w}, b) = -\sum_{i=1}^n \left[y^{(i)} \log(\sigma(z^{(i)})) + (1 - y^{(i)}) \log(1 - \sigma(z^{(i)}))\right] J(w,b)=i=1n[y(i)log(σ(z(i)))+(1y(i))log(1σ(z(i)))]

    其中 β 0 \beta_0 β0就是b,w就是 β 1 到 β n \beta_1 到 \beta_n β1βn,z就是线性组合 β 0 + β 1 X 1 + β 2 X 2 + β 3 X 3 + β 4 X 4 \beta_0 + \beta_1X_1 + \beta_2X_2 + \beta_3X_3 + \beta_4X_4 β0+β1X1+β2X2+β3X3+β4X4 σ \sigma σ就是sigmoid函数。
    现在我们的目标就是最小化上面的公式,要求它的最小值对应的 β \beta β,我们就又需要用到梯度下降法了。
    在梯度下降法中,我们更新权重 w \mathbf{w} w 和偏置 b b b 以最小化损失 J J J,更新规则为:

    w ← w − α ∇ w J \mathbf{w} \leftarrow \mathbf{w} - \alpha \nabla_{\mathbf{w}} J wwαwJ

    b ← b − α ∂ J ∂ b b \leftarrow b - \alpha \frac{\partial J}{\partial b} bbαbJ

    所以现在我们就先要对上面的公式求导,求出梯度,然后再用梯度下降法来更新 β \beta β

    β 0 \beta_0 β0 的偏导数(对于偏置项)

    1. 写出损失函数 J J J
      J ( w , b ) = − ∑ i = 1 n [ y ( i ) log ⁡ ( σ ( z ( i ) ) ) + ( 1 − y ( i ) ) log ⁡ ( 1 − σ ( z ( i ) ) ) ] J(w, b) = -\sum_{i=1}^n \left[y^{(i)} \log(\sigma(z^{(i)})) + (1 - y^{(i)}) \log(1 - \sigma(z^{(i)}))\right] J(w,b)=i=1n[y(i)log(σ(z(i)))+(1y(i))log(1σ(z(i)))]
    2. 写出 z ( i ) z^{(i)} z(i)
      z ( i ) = β 0 + β 1 x 1 ( i ) + … + β n x n ( i ) z^{(i)} = \beta_0 + \beta_1 x_1^{(i)} + \ldots + \beta_n x_n^{(i)} z(i)=β0+β1x1(i)++βnxn(i)
    3. z ( i ) z^{(i)} z(i) β 0 \beta_0 β0 的偏导数:
      ∂ z ( i ) ∂ β 0 = 1 \frac{\partial z^{(i)}}{\partial \beta_0} = 1 β0z(i)=1
    4. 对 Sigmoid 函数求导:
      d σ d z = σ ( z ) ⋅ ( 1 − σ ( z ) ) \frac{d\sigma}{dz} = \sigma(z) \cdot (1 - \sigma(z)) dzdσ=σ(z)(1σ(z))
      其中, σ ( z ) = 1 1 + e − z \sigma(z) = \frac{1}{1 + e^{-z}} σ(z)=1+ez1
    5. 对损失函数 J J J 关于 σ ( z ( i ) ) \sigma(z^{(i)}) σ(z(i)) 求偏导数:
      ∂ J ∂ σ ( z ( i ) ) = − ( y ( i ) σ ( z ( i ) ) − 1 − y ( i ) 1 − σ ( z ( i ) ) ) \frac{\partial J}{\partial \sigma(z^{(i)})} = -\left(\frac{y^{(i)}}{\sigma(z^{(i)})} - \frac{1 - y^{(i)}}{1 - \sigma(z^{(i)})}\right) σ(z(i))J=(σ(z(i))y(i)1σ(z(i))1y(i))
    6. 应用链式法则得到 J J J 关于 β 0 \beta_0 β0 的偏导数:
      ∂ J ∂ β 0 = ∑ i = 1 n ∂ J ∂ σ ( z ( i ) ) ⋅ d σ d z ( i ) ⋅ ∂ z ( i ) ∂ β 0 \frac{\partial J}{\partial \beta_0} = \sum_{i=1}^n \frac{\partial J}{\partial \sigma(z^{(i)})} \cdot \frac{d\sigma}{dz^{(i)}} \cdot \frac{\partial z^{(i)}}{\partial \beta_0} β0J=i=1nσ(z(i))Jdz(i)dσβ0z(i)
    7. 计算最终的偏导数表达式:
      ∂ J ∂ β 0 = ∑ i = 1 n ( σ ( z ( i ) ) − y ( i ) ) \frac{\partial J}{\partial \beta_0} = \sum_{i=1}^n \left(\sigma(z^{(i)}) - y^{(i)}\right) β0J=i=1n(σ(z(i))y(i))

    β j \beta_j βj 的偏导数(对于权重项)

    1. z ( i ) z^{(i)} z(i) β j \beta_j βj 的偏导数:
      ∂ z ( i ) ∂ β j = x j ( i ) \frac{\partial z^{(i)}}{\partial \beta_j} = x_j^{(i)} βjz(i)=xj(i)

    2. 使用链式法则得到 J J J 关于 β j \beta_j βj 的偏导数:
      ∂ J ∂ β j = ∑ i = 1 n ∂ J ∂ σ ( z ( i ) ) ⋅ d σ d z ( i ) ⋅ ∂ z ( i ) ∂ β j \frac{\partial J}{\partial \beta_j} = \sum_{i=1}^n \frac{\partial J}{\partial \sigma(z^{(i)})} \cdot \frac{d\sigma}{dz^{(i)}} \cdot \frac{\partial z^{(i)}}{\partial \beta_j} βjJ=i=1nσ(z(i))Jdz(i)dσβjz(i)

    3. 计算最终的偏导数表达式:
      ∂ J ∂ β j = ∑ i = 1 n ( σ ( z ( i ) ) − y ( i ) ) x j ( i ) \frac{\partial J}{\partial \beta_j} = \sum_{i=1}^n \left(\sigma(z^{(i)}) - y^{(i)}\right) x_j^{(i)} βjJ=i=1n(σ(z(i))y(i))xj(i)

      在实际的算法实现中,这些导数会用来更新 β 0 \beta_0 β0 β j \beta_j βj

    因为 σ ( z ( i ) ) \sigma(z^{(i)}) σ(z(i))其实就是我们的预测值,所以这个公式的意思就是我们的预测值减去实际值,然后再求和,就是我们的偏导数。

    β 0 : = β 0 − α ∂ J ∂ β 0 \beta_0 := \beta_0 - \alpha \frac{\partial J}{\partial \beta_0} β0:=β0αβ0J
    β j : = β j − α ∂ J ∂ β j \beta_j := \beta_j - \alpha \frac{\partial J}{\partial \beta_j} βj:=βjαβjJ

    1. 定义 Sigmoid 函数: 这是逻辑回归中的核心函数,将线性组合映射到 (0, 1) 区间。
    2. 定义损失函数: 这个函数计算当前权重和偏置下的对数损失。
    3. 定义梯度计算函数: 计算损失函数关于每个参数的梯度。
    4. 定义梯度下降更新规则: 使用计算得到的梯度更新权重和偏置。
    5. 执行训练过程: 迭代执行梯度下降步骤,直至满足收敛条件或达到指定的迭代次数。
    # 定义 Sigmoid 函数
    def sigmoid(z):
        return 1 / (1 + np.exp(-z))
    
    • 1
    • 2
    • 3

    定义损失函数
    公式为:J(w, b) = − ∑ i = 1 n [ y ( i ) log ⁡ ( σ ( z ( i ) ) + ( 1 − y ( i ) ) log ⁡ ( 1 − σ ( z ( i ) ) ) ] -\sum_{i=1}^n [y^{(i)} \log(\sigma(z^{(i)}) + (1 - y^{(i)}) \log(1 - \sigma(z^{(i))})] i=1n[y(i)log(σ(z(i))+(1y(i))log(1σ(z(i)))]

    # 定义对数损失函数——我们的目的就是让损失最小,就是让似然函数最大,当求的这些beta能够使得损失最小的时候,就是我们的最优解,beta就是我们的最优参数
    def compute_loss(X, y, w, b):
        # 对应上面的z=(β0+β1X1+β2X2+β3X3+β4X4)
        z = np.dot(X, w) + b 
        probs = sigmoid(z)
        # np.mean()求损失的均值
        loss = -np.mean(y * np.log(probs) + (1 - y) * np.log(1 - probs))
        return loss
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在 compute_gradients 函数中,参数的含义如下:

    X: 特征数据矩阵。它是一个二维数组,其中每一行代表一个训练样本,每一列代表一个特征。
    y: 标签向量。它是一个一维数组,包含与特征矩阵 X 中的每一行相对应的标签。在二分类逻辑回归中,这些标签通常是 0 或 1。
    w: 权重向量。这是一个一维数组,其中每个元素对应于 X 矩阵中某个特征的权重。
    b: 偏置项。这是一个标量,它与权重向量 w 一起决定了模型的预测。

    # 定义梯度计算函数——这个函数就是对上面的损失函数求导,求出梯度
    # w对应beta1到beta4,b对应beta0,X对应X1到X4,y对应Y(实际的值)
    def compute_gradients(X, y, w, b):
        z = np.dot(X, w) + b
        probs = sigmoid(z)
        error = probs - y # 对应sigmoid(z)-y
        # 梯度计算,就是前面推导的公式
        grad_w = np.dot(X.T, error) / len(y) # 对应(1/n)*sum((sigmoid(z)-y)*X)
        grad_b = np.mean(error) # 对应(1/n)*sum(sigmoid(z)-y)
        return grad_w, grad_b
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    # 定义梯度下降函数
    def gradient_descent(X, y, w, b, alpha, iterations):
        '''
        :param X: 表示特征数据矩阵
        :param y: 表示标签向量(实际值)
        :param w: 权重向量
        :param b: 偏置项
        :param alpha: 学习率——就是我们的步长,每一次更新多少,是一个自己定义的参数
        :param iterations: 迭代次数——就是我们的梯度下降要迭代多少次
        :return: 返回最终的权重和偏置
        '''
        for i in range(iterations):
            grad_w, grad_b = compute_gradients(X, y, w, b)
            # 更新权重和偏置
            w -= alpha * grad_w
            b -= alpha * grad_b
            # 每迭代一定次数打印损失
            if i % 100 == 0:
                print(f"Iteration {i}: Loss = {compute_loss(X, y, w, b)}")
        return w, b
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    准备数据

    • X 1 X_1 X1: 年龄
    • X 2 X_2 X2: 性别(0表示女性,1表示男性)
    • X 3 X_3 X3: 体重指数(BMI)
    • X 4 X_4 X4: 血糖水平
    • Y Y Y: 是否有糖尿病(0表示没有,1表示有)
    # 准备数据
    # x = df的前四列,y = df的最后一列
    X = df.iloc[:, :-1].values # iloc是通过行号来取行数据的,这里取所有行,去掉最后一列
    y = df.iloc[:, -1].values # 取所有行,取最后一列
    X.shape, y.shape
    
    • 1
    • 2
    • 3
    • 4
    • 5
    ((1000, 4), (1000,))
    
    • 1
    # 将X和y分为训练集和测试集
    from sklearn.model_selection import train_test_split
    # 这里就是把X和y分为训练集和测试集,test_size=0.2表示测试集占20%,random_state=0表示随机种子,保证每次运行结果一样
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0)
    
    • 1
    • 2
    • 3
    • 4
    # 特征标准化——目的是让数据的均值为0,方差为1,这样可以加快梯度下降的收敛速度
    from sklearn.preprocessing import StandardScaler
    scaler = StandardScaler()
    X_train = scaler.fit_transform(X_train)
    X_test = scaler.transform(X_test)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    # 初始化权重和偏置
    # 这里的w是一个一维数组,长度为X_train的列数,也就是特征的个数,这里是4
    w = np.zeros(X_train.shape[1])
    b = 0
    
    • 1
    • 2
    • 3
    • 4
    # 训练模型
    # 这里的alpha是学习率,iterations是迭代次数
    w, b = gradient_descent(X_train, y_train, w, b, alpha=0.1, iterations=10000)
    
    • 1
    • 2
    • 3
    Iteration 0: Loss = 0.67694848293987
    Iteration 100: Loss = 0.31170512579698983
    Iteration 200: Loss = 0.28006450564964497
    Iteration 300: Loss = 0.26967717122142904
    Iteration 400: Loss = 0.2649489155820069
    Iteration 500: Loss = 0.26246507021700166
    Iteration 600: Loss = 0.2610482148241369
    Iteration 700: Loss = 0.26019578953398675
    .........................................
    Iteration 8300: Loss = 0.2586331617938211
    Iteration 8400: Loss = 0.25863316179381984
    Iteration 8500: Loss = 0.2586331617938189
    Iteration 8600: Loss = 0.25863316179381834
    Iteration 8700: Loss = 0.25863316179381785
    Iteration 8800: Loss = 0.2586331617938175
    Iteration 8900: Loss = 0.2586331617938173
    Iteration 9000: Loss = 0.2586331617938171
    Iteration 9100: Loss = 0.25863316179381696
    Iteration 9200: Loss = 0.25863316179381696
    Iteration 9300: Loss = 0.2586331617938169
    Iteration 9400: Loss = 0.25863316179381685
    Iteration 9500: Loss = 0.25863316179381685
    Iteration 9600: Loss = 0.2586331617938168
    Iteration 9700: Loss = 0.2586331617938168
    Iteration 9800: Loss = 0.2586331617938168
    Iteration 9900: Loss = 0.2586331617938168
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    # 训练好模型后,我们可以使用它来进行预测
    # 预测概率
    test_probs = sigmoid(np.dot(X_test, w) + b)
    # 预测类别
    test_preds = np.where(test_probs > 0.5, 1, 0)
    test_preds
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1,
           1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
           1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1,
           1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1,
           1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
           1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1,
           1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
           1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0,
           1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
           1, 1])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    # 计算准确率
    from sklearn.metrics import accuracy_score
    accuracy = accuracy_score(y_test, test_preds)
    accuracy
    
    • 1
    • 2
    • 3
    • 4
    0.885
    
    • 1
    # 比较真实值和预测值
    results = pd.DataFrame({
        'Actual': y_test,
        'Predicted': test_preds,
        'Probability': test_probs
    })
    results.head(100)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    ActualPredictedProbability
    0110.974574
    1110.961203
    2110.874061
    3110.996859
    4110.832528
    ............
    95010.873257
    96110.944671
    97110.903943
    98110.979292
    99110.998257

    100 rows × 3 columns

    # 最终的权重和偏置
    print(f"Weight: {w}")
    print(f"Bias: {b}")
    # 最终准确度
    print(f"Final Accuracy: {accuracy}")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    Weight: [1.41949725 0.42282911 0.70729532 0.90150334]
    Bias: 3.0447132565927673
    Final Accuracy: 0.885
    
    • 1
    • 2
    • 3

    总结

    在这个项目中,我们手动实现了逻辑回归模型的训练过程。我们首先定义了 Sigmoid 函数和对数损失函数,然后使用梯度下降法最小化损失函数。我们将这个模型应用于一个模拟数据集,其中包含患者的生理指标和是否患有糖尿病的标签。最终,我们评估了模型的准确性,并展示了预测结果。
    通过最终的结果,我们可以看到我们的模型在测试集上的准确率为 0.885,说明我们的逻辑回归模型在这个数据集上表现还是不错的。

    补充——采用sklearn库实现逻辑回归
    # 使用sklearn库实现逻辑回归
    from sklearn.linear_model import LogisticRegression
    # 创建逻辑回归模型
    model = LogisticRegression()
    # 训练模型
    model.fit(X_train, y_train)
    # 预测
    y_pred = model.predict(X_test)
    # 计算准确率
    accuracy = accuracy_score(y_test, y_pred)
    accuracy
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    0.885
    
    • 1
    # 比较调库实现的逻辑回归和手动实现的逻辑回归的权重和偏置
    print("Manual Model:")
    print(f"Weight: {w}")
    print(f"Bias: {b}")
    print("\nSklearn Model:")
    print(f"Weight: {model.coef_}")
    print(f"Bias: {model.intercept_}")
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    Manual Model:
    Weight: [1.41949725 0.42282911 0.70729532 0.90150334]
    Bias: 3.0447132565927673
    
    Sklearn Model:
    Weight: [[1.37051336 0.40978953 0.68512706 0.87338347]]
    Bias: [2.988773]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    可以发现,两者的权重和偏置非常接近,这说明我们手动实现的逻辑回归模型是正确的。同时,我们也可以看到,使用sklearn库实现的逻辑回归模型的准确率与我们手动实现的模型准确率相同,这说明我们的模型在这个数据集上表现良好。
  • 相关阅读:
    我常用的两个翻译神器,程序员必备.
    解决Linux服务器中docker访问报127.0.0.1:2375拒绝连接 (Connection refused)的问题
    倒排索引:ES倒排索引底层原理及FST算法的实现过程
    若依(ruoyi)开源系统保姆级实践-完成第一个页面
    SpringBoot整合knife4j
    CTFshow 信息收集 web 6 7 8 9 10
    ITIL 4指导、计划和改进—评估和计划
    VuePress实现自动获取文章侧边栏目录功能
    信息学奥赛一本通 1339:【例3-4】求后序遍历 | 洛谷 P1827 [USACO3.4] 美国血统 American Heritage
    Docker容器的数据卷
  • 原文地址:https://blog.csdn.net/m0_60388871/article/details/138153052