#P4344. 第3题-商品购买预测
-
1000ms
Tried: 17
Accepted: 2
Difficulty: 6
所属公司 :
华为
时间 :2025年10月29日-AI方向
-
算法标签>批量梯度下降交叉熵损失L2 正则化
第3题-商品购买预测
解题思路
本题要求用二分类逻辑回归(Logistic Regression)对“是否购买”进行预测。模型形式为 y^=σ(z)=σ(w⊤x+b),其中 σ(t)=1+e−t1 为 Sigmoid。
优化目标(带 L2 正则的交叉熵): 对 n 个样本,特征维度为 d=3(年龄、月收入、浏览时长),标签 y∈{0,1}。 损失函数取平均交叉熵并加入 L2 正则(采用更常见的平方范数形式):
$$J(w,b)=\frac{1}{n}\sum_{i=1}^n\Big[-y_i\log \hat y_i-(1-y_i)\log(1-\hat y_i)\Big]+\frac{\lambda}{2n}\|w\|_2^2 $$其中 y^i=σ(w⊤xi+b)。
梯度推导: 记 pi=y^i,则
$$\frac{\partial J}{\partial w}=\frac{1}{n}\sum_{i=1}^n (p_i-y_i)\,x_i+\frac{\lambda}{n}w,\qquad \frac{\partial J}{\partial b}=\frac{1}{n}\sum_{i=1}^n (p_i-y_i). $$训练(批量梯度下降):
- 初始化 w=0,b=0;
- 迭代更新 w←w−α∂J/∂w, b←b−α∂J/∂b;
- 终止条件:达到最大迭代次数
max_iter或 先更新参数,再计算一次新损失,用相邻两次损失之差判断是否 <tol; - 数值稳定性:交叉熵内的 log 对概率做 ε 截断(如 1e−15),Sigmoid 采用分段公式避免上溢/下溢。
预测:
- 概率 p=σ(w⊤x+b);
- 阈值 0.5:p≥0.5 判为 1,否则 0;
- 输出格式为“预测结果 概率(四位小数)”。
复杂度分析
- 时间复杂度:每次迭代需扫一遍数据并做 d 次累计,故为 O(max_iter×n×d)。本题 d=3,复杂度适宜。
- 空间复杂度:存储数据与参数,O(n×d)+O(d)。参数仅 O(d);数据为输入必须项。
代码实现
Python
import sys
import math
# Sigmoid 函数,数值稳定写法
def sigmoid(z):
if z >= 0:
ez = math.exp(-z)
return 1.0 / (1.0 + ez)
else:
ez = math.exp(z)
return ez / (1.0 + ez)
# 计算带 L2 正则(平方范数)的平均交叉熵损失及其梯度
def compute_loss_and_grad(X, y, w, b, lam):
n = len(X)
d = len(w)
eps = 1e-15
loss = 0.0
grad_w = [0.0] * d
grad_b = 0.0
# 累加交叉熵与梯度
for i in range(n):
z = b
for j in range(d):
z += w[j] * X[i][j]
p = sigmoid(z)
yi = y[i]
# 交叉熵
loss += -(yi * math.log(max(p, eps)) + (1 - yi) * math.log(max(1 - p, eps)))
# 梯度累加
diff = p - yi
for j in range(d):
grad_w[j] += diff * X[i][j]
grad_b += diff
# 取平均
loss /= n
for j in range(d):
grad_w[j] /= n
grad_b /= n
# L2 正则项:lambda/(2n)*||w||^2,并相应修正梯度
l2 = 0.0
for j in range(d):
l2 += w[j] * w[j]
grad_w[j] += (lam / n) * w[j]
loss += (lam / (2 * n)) * l2
return loss, grad_w, grad_b
# 训练逻辑回归(批量梯度下降)
def train_logreg(X, y, max_iter, alpha, lam, tol):
d = len(X[0])
w = [0.0] * d
b = 0.0
# 先算一次初始损失
loss, _, _ = compute_loss_and_grad(X, y, w, b, lam)
for _ in range(max_iter):
# 基于当前参数计算梯度并更新
_, grad_w, grad_b = compute_loss_and_grad(X, y, w, b, lam)
for j in range(d):
w[j] -= alpha * grad_w[j]
b -= alpha * grad_b
# 更新后再计算一次新损失,用于提前停止判断
new_loss, _, _ = compute_loss_and_grad(X, y, w, b, lam)
if abs(loss - new_loss) < tol:
break
loss = new_loss
return w, b
# 单样本预测
def predict_one(x, w, b):
z = b
for j in range(len(w)):
z += w[j] * x[j]
p = sigmoid(z)
label = 1 if p >= 0.5 else 0
return label, p
def main():
data = sys.stdin.read().strip().split()
it = iter(data)
# 读取第一行
n = int(next(it))
max_iter = int(next(it))
alpha = float(next(it))
lam = float(next(it))
tol = float(next(it))
# 训练数据
X = []
y = []
for _ in range(n):
a = float(next(it))
inc = float(next(it))
dur = float(next(it))
lab = int(next(it))
X.append([a, inc, dur])
y.append(lab)
# 测试数据
m = int(next(it))
test = []
for _ in range(m):
a = float(next(it)); inc = float(next(it)); dur = float(next(it))
test.append([a, inc, dur])
# 训练
w, b = train_logreg(X, y, max_iter, alpha, lam, tol)
# 预测与输出
for x in test:
lab, p = predict_one(x, w, b)
print(f"{lab} {p:.4f}")
if __name__ == "__main__":
main()
Java
import java.util.*;
/**
* 逻辑回归(批量梯度下降,Sigmoid,交叉熵,L2 正则)
* 输入输出均在主函数中完成,核心训练与预测在外部静态函数中
*/
public class Main {
// Sigmoid,分段实现避免溢出
static double sigmoid(double z) {
if (z >= 0) {
double ez = Math.exp(-z);
return 1.0 / (1.0 + ez);
} else {
double ez = Math.exp(z);
return ez / (1.0 + ez);
}
}
// 计算损失与梯度(带 L2 正则的平方范数)
static double[] computeLossAndGrad(double[][] X, int[] y, double[] w, double b, double lam) {
int n = X.length;
int d = w.length;
double eps = 1e-15;
double loss = 0.0;
double[] gradW = new double[d];
double gradB = 0.0;
for (int i = 0; i < n; i++) {
double z = b;
for (int j = 0; j < d; j++) z += w[j] * X[i][j];
double p = sigmoid(z);
int yi = y[i];
// 交叉熵
double pp = Math.max(p, eps);
double qq = Math.max(1.0 - p, eps);
loss += -(yi * Math.log(pp) + (1 - yi) * Math.log(qq));
// 梯度
double diff = p - yi;
for (int j = 0; j < d; j++) gradW[j] += diff * X[i][j];
gradB += diff;
}
// 平均
loss /= n;
for (int j = 0; j < d; j++) gradW[j] /= n;
gradB /= n;
// L2 正则
double l2 = 0.0;
for (int j = 0; j < d; j++) {
l2 += w[j] * w[j];
gradW[j] += (lam / n) * w[j];
}
loss += (lam / (2.0 * n)) * l2;
// 返回一个数组: [loss, gradB, gradW...]
double[] res = new double[2 + d];
res[0] = loss;
res[1] = gradB;
for (int j = 0; j < d; j++) res[2 + j] = gradW[j];
return res;
}
// 训练
static double[] train(double[][] X, int[] y, int maxIter, double alpha, double lam, double tol) {
int d = X[0].length;
double[] w = new double[d];
double b = 0.0;
// 初始损失
double[] pack0 = computeLossAndGrad(X, y, w, b, lam);
double loss = pack0[0];
for (int it = 0; it < maxIter; it++) {
double[] pack = computeLossAndGrad(X, y, w, b, lam);
double gradB = pack[1];
for (int j = 0; j < d; j++) {
double gradWj = pack[2 + j];
w[j] -= alpha * gradWj;
}
b -= alpha * gradB;
double[] packNew = computeLossAndGrad(X, y, w, b, lam);
double newLoss = packNew[0];
if (Math.abs(loss - newLoss) < tol) break;
loss = newLoss;
}
double[] params = new double[d + 1];
params[0] = b;
for (int j = 0; j < d; j++) params[1 + j] = w[j];
return params;
}
// 单样本预测
static double predictProb(double[] x, double[] w, double b) {
double z = b;
for (int j = 0; j < w.length; j++) z += w[j] * x[j];
return sigmoid(z);
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int maxIter = sc.nextInt();
double alpha = sc.nextDouble();
double lam = sc.nextDouble();
double tol = sc.nextDouble();
double[][] X = new double[n][3];
int[] y = new int[n];
// 读训练数据
for (int i = 0; i < n; i++) {
X[i][0] = sc.nextDouble(); // 年龄
X[i][1] = sc.nextDouble(); // 月收入(千元)
X[i][2] = sc.nextDouble(); // 浏览时长(分钟)
y[i] = sc.nextInt(); // 标签
}
int m = sc.nextInt();
double[][] T = new double[m][3];
for (int i = 0; i < m; i++) {
T[i][0] = sc.nextDouble();
T[i][1] = sc.nextDouble();
T[i][2] = sc.nextDouble();
}
sc.close();
// 训练
double[] params = train(X, y, maxIter, alpha, lam, tol);
double b = params[0];
double[] w = new double[params.length - 1];
for (int j = 0; j < w.length; j++) w[j] = params[1 + j];
// 预测与输出
for (int i = 0; i < m; i++) {
double p = predictProb(T[i], w, b);
int lab = (p >= 0.5) ? 1 : 0;
System.out.printf("%d %.4f%n", lab, p);
}
}
}
C++
#include <bits/stdc++.h>
using namespace std;
// Sigmoid,分段实现避免溢出
double sigmoid(double z) {
if (z >= 0) {
double ez = exp(-z);
return 1.0 / (1.0 + ez);
} else {
double ez = exp(z);
return ez / (1.0 + ez);
}
}
// 计算损失与梯度(带 L2 正则的平方范数)
// 返回 pair<loss, pair<grad_b, grad_w>>
pair<double, pair<double, vector<double>>> computeLossAndGrad(
const vector<array<double,3>>& X,
const vector<int>& y,
const vector<double>& w,
double b,
double lam
){
int n = (int)X.size();
int d = (int)w.size();
double eps = 1e-15;
double loss = 0.0;
vector<double> grad_w(d, 0.0);
double grad_b = 0.0;
for (int i = 0; i < n; ++i) {
double z = b;
for (int j = 0; j < d; ++j) z += w[j] * X[i][j];
double p = sigmoid(z);
int yi = y[i];
// 交叉熵
double pp = max(p, eps);
double qq = max(1.0 - p, eps);
loss += -(yi * log(pp) + (1 - yi) * log(qq));
// 梯度
double diff = p - yi;
for (int j = 0; j < d; ++j) grad_w[j] += diff * X[i][j];
grad_b += diff;
}
// 平均
loss /= n;
for (int j = 0; j < d; ++j) grad_w[j] /= n;
grad_b /= n;
// L2 正则
double l2 = 0.0;
for (int j = 0; j < d; ++j) {
l2 += w[j] * w[j];
grad_w[j] += (lam / n) * w[j];
}
loss += (lam / (2.0 * n)) * l2;
return {loss, {grad_b, grad_w}};
}
// 训练逻辑回归(批量梯度下降)
pair<vector<double>, double> train(
const vector<array<double,3>>& X,
const vector<int>& y,
int max_iter, double alpha, double lam, double tol
){
int d = 3;
vector<double> w(d, 0.0);
double b = 0.0;
// 初始损失
auto initPack = computeLossAndGrad(X, y, w, b, lam);
double loss = initPack.first;
for (int it = 0; it < max_iter; ++it) {
auto res = computeLossAndGrad(X, y, w, b, lam);
double grad_b = res.second.first;
const vector<double>& grad_w = res.second.second;
for (int j = 0; j < d; ++j) w[j] -= alpha * grad_w[j];
b -= alpha * grad_b;
auto resNew = computeLossAndGrad(X, y, w, b, lam);
double new_loss = resNew.first;
if (fabs(loss - new_loss) < tol) break;
loss = new_loss;
}
return {w, b};
}
// 单样本预测
pair<int,double> predict_one(const array<double,3>& x, const vector<double>& w, double b) {
double z = b;
for (int j = 0; j < (int)w.size(); ++j) z += w[j] * x[j];
double p = sigmoid(z);
int lab = (p >= 0.5) ? 1 : 0;
return {lab, p};
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, max_iter;
double alpha, lam, tol;
if (!(cin >> n >> max_iter >> alpha >> lam >> tol)) return 0;
vector<array<double,3>> X(n);
vector<int> y(n);
for (int i = 0; i < n; ++i) {
double a, inc, dur; int lab;
cin >> a >> inc >> dur >> lab;
X[i] = {a, inc, dur};
y[i] = lab;
}
int m; cin >> m;
vector<array<double,3>> T(m);
for (int i = 0; i < m; ++i) {
double a, inc, dur;
cin >> a >> inc >> dur;
T[i] = {a, inc, dur};
}
// 训练
auto model = train(X, y, max_iter, alpha, lam, tol);
vector<double> w = model.first;
double b = model.second;
cout.setf(std::ios::fixed);
cout << setprecision(4);
// 预测与输出
for (int i = 0; i < m; ++i) {
auto pr = predict_one(T[i], w, b);
cout << pr.first << " " << pr.second << "\n";
}
return 0;
}
题目内容
实现一个二分类逻辑回归模型,用于预测用户是否会购买某商品(1 表示购买,0 表示不购买)。已知用户特征包括年龄(岁)、月收入(千元)和浏览时长(分钟),需通过这些特征建立预测模型。
我们构建一个逻辑回归模型来预测购买:ypred=sigmoid(wx+b),
w 和 b 分别为特征权重和偏置。
要求:
- 基于训练数据,使用梯度下降法训练逻辑回归模型参数。要求采用的具体函数方法为:损失函数为预测结果与标签值的交叉熵
$L = \frac{1}{n} \sum_{i=1}^{n} CrossEntropy(y_i^{pred}, y_i^{label})$,使用 L2 正则约束权重 L2(w)=∣∣w∣∣2,激活函数为 Sigmoid 。
训练终止条件:迭代次数达到最大次数(max_iter)或损失函数变化量小于阈值(tol)。
对测试数据进行预测,输出预测结果(概率 ≥0.5 预测为 1,否则为 0),和对应的概率值,四舍五入保留四位小数。
输入描述
第一行:5 个数字,分别为训练样本数 n 、最大迭代次数 max_iter、学习率 α(浮点型)、正则化系数 λ(浮点型)、损失阈值 tol(浮点型)。
接下来 n 行:每行 4 个数值,前 3 个为特征(年龄、月收入、浏览时长),第 4 个为标签(0 或 1)。
第 n+2 行:整数 m(测试样本数)。
接下来 m 行:每行 3 个数值,为测试特征。
输出描述
m 行,每行 1 个整数( 0 或 1),表示预测结果;接着是一个空格;紧接着输出对应的商品购买小数概率值(保留四位小数)
样例1
输入
10 1000 0.01 0.1 0.0001
25 8 5 0
30 15 15 1
35 20 10 0
40 25 20 1
45 30 25 1
50 35 18 1
55 40 12 0
60 45 8 0
65 50 5 0
70 55 30 1
3
32 18 12
48 33 22
62 48 10
输出
1 0.7539
1 0.9966
0 0.0004
说明
对于第一个样例,年龄(32 岁)、月收入(18 千元)和浏览时长(12 分钟),输出结果 1 表示该用户会购买该商品,0.7539 表示用户购买该商品的概率是 75.39%。
样例2
输入
10 1000 0.01 0.1 0.0001
25 8 5 0
30 15 15 1
35 20 10 0
40 25 20 1
45 30 25 1
50 35 18 1
55 40 12 0
60 45 8 0
65 50 5 0
70 55 30 1
1
48 30 10
输出
0 0.0081
说明
对于第一个样例,年龄(48 岁)、月收入(30 千元)和浏览时长(10 分钟),输出结果 0 表示该用户不会购买该商品,0.0081 表示该用户购买该商品的概率是 0.81%。