news 2026/10/11 7:36:48

从零手写Logistic Regression:揭开神经网络最小单元的核心原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零手写Logistic Regression:揭开神经网络最小单元的核心原理

1. 为什么深度学习基础课要专门用一讲来过Logistic Regression

很多刚接触深度学习的同学其实都有一个困惑:Logistic Regression不是机器学习里的经典分类算法吗,怎么会被塞进深度学习基础的课程里,还专门占了一讲?

我带过不少实习生,这个疑问几乎每次都出现。我的回答很简单:当你真正动手用Python实现一次Logistic Regression,你其实就在实现一个最简单的神经网络——一个没有隐藏层、只有一个神经元、激活函数是sigmoid的二分类网络。深度学习里面那些看似复杂的CNN、RNN、Transformer,拆到底层,都是由这种"线性变换 + 非线性激活"的单元堆叠出来的。你把这个最小单元吃透了,后面理解反向传播、梯度消失、Batch Norm,会顺很多。

这个内容适合两类人:一类是刚入门深度学习、希望在正式碰神经网络之前先把基础夯实的学习者;另一类是已经会用sklearn调包做分类,但从来没手写过梯度下降、心里始终不踏实的从业者。这篇博文的目的,就是带你把Logistic Regression的每一行代码、每一个数学公式背后的直觉全部打通。

我先说一个反直觉的结论:Logistic Regression虽然名字里带着"Regression",但它解决的是分类问题,而不是回归问题。它做的事情是:给定一组特征,输出一个0到1之间的概率值,然后你通过一个阈值(通常是0.5)来决定样本属于哪个类别。之所以叫"Regression",是因为它在数学形式上确实是回归——线性回归算出的是一个连续实数,Logistic Regression先算出连续实数,再用sigmoid函数把它压到(0,1)区间,变成概率。

从深度学习的视角看,它的结构清晰得令人发指:

  • 输入层:特征向量 x
  • 权重:w,偏置:b
  • 线性变换:z = w^T x + b
  • 激活函数:σ(z) = 1 / (1 + e^(-z))
  • 输出:预测概率 ŷ

就这五步。没有任何隐藏层,没有复杂的网络拓扑,但它已经包含了深度学习模型的全部要素:参数、前向传播、损失函数、反向传播、参数更新。你在后续所有深度学习模型里做的事情,本质上都是这套流程的放大和变形。

所以我的建议很直接:不要跳过这一节。哪怕你已经能跑通一个完整的CNN模型,回头来亲手实现一次Logistic Regression,你一定会有新的收获——尤其是当你真正开始调试梯度下降的时候。

2. 一个完整模型的全部零件:前向计算、损失函数和梯度推导

这一节我们不看代码,先把数学和直觉打通。因为如果你不理解为什么损失函数是交叉熵而不是均方误差,不理解梯度是怎么一步步传回来的,你后面调试代码时根本不知道问题出在哪里。

2.1 前向计算:从输入到概率

对于单个样本 x ∈ R^n,前向计算分两步:

第一步,线性部分:

z = w_1 x_1 + w_2 x_2 + ... + w_n x_n + b = w^T x + b

这就是线性回归的表达式。它的意思是:把输入特征的加权和算出来,得到一个实数 z。这个数可以是负的、正的,范围没有限制。

第二步,非线性部分:

ŷ = σ(z) = 1 / (1 + e^(-z))

sigmoid函数做的事情,是把任意一个实数 z 映射到 (0, 1) 区间。当 z 趋向正无穷,σ(z) 趋向1;当 z 趋向负无穷,σ(z) 趋向0;当 z=0,σ(z)=0.5。

为什么分类问题需要这个非线性映射?因为分类任务的输出应该是概率,概率天然在[0,1]区间内。线性回归直接输出一个无界的实数,没法解释成概率。sigmoid就是那个"压缩器",把无界的分数压成有界的概率。

这里我多说一句:sigmoid并不是唯一的选择,但在二分类问题里,它是和"概率解释"最搭的激活函数。这与概率论里的log-odds(对数几率)概念直接相关。ln(ŷ/(1-ŷ)) = z,也就是说,逻辑回归的线性输出 z 实际上是事件发生几率的对数。这是Logistic Regression这个名称的来源,也是它之所以在数学上自洽的根本原因。

2.2 为什么损失函数必须用交叉熵,而不是均方误差

训练模型需要定义损失函数。很多人第一反应会用MSE(均方误差):

L = (1/2)(ŷ - y)^2

这个函数在回归问题里没问题,但在分类问题里,它有致命的缺陷。我来算给你看。

如果用MSE,对参数 w 求梯度,会得到:

∂L/∂w = (ŷ - y) · σ'(z) · x

而 σ'(z) = σ(z)(1 - σ(z))。当 σ(z) 接近0或1时,σ'(z) 会非常接近0。这是sigmoid函数的固有属性:它在两端极度饱和。一旦模型的预测已经"比较自信"(比如ŷ=0.95),梯度就会被乘上一个很小的数(0.95×0.05 ≈ 0.0475),参数更新变得极其缓慢。这就是所谓的梯度消失问题。不是深度网络才有的,在单层逻辑回归上就已经出现了。

交叉熵损失函数长这样:

L = -[y · ln(ŷ) + (1-y) · ln(1-ŷ)]

它的梯度算出来是什么效果?先对 z 求偏导。用链式法则:

∂L/∂z = ∂L/∂ŷ · ∂ŷ/∂z

先算 ∂L/∂ŷ = -(y/ŷ) + (1-y)/(1-ŷ) · (-1),整理一下:

∂L/∂ŷ = (ŷ - y) / (ŷ(1-ŷ))

而 ∂ŷ/∂z = ŷ(1-ŷ),两者一乘,中间的 ŷ(1-ŷ) 恰好消掉了:

∂L/∂z = ŷ - y

多么漂亮的结果。梯度变成了"预测值减去真实值",这个值在ŷ越离谱时越大,模型更新得越快;在ŷ接近正确时趋近于0,模型更新得慢。不存在梯度消失问题,而且梯度的大小和误差大小成正比。这就是逻辑回归必须用交叉熵的根本原因。

用生活化的方式理解:MSE像是一个"惩罚一切差异"的严苛老师,但它在学生已经错得很离谱时反而惩罚力度变弱了(因为sigmoid饱和);交叉熵不一样,它只关心你的预测和真实标签是否一致,错得越远,梯度越大,更新越狠。这个特性使得交叉熵加sigmoid的组合成为深度学习中输出层分类任务的标准配置,直到今天都没有被替代。

2.3 梯度下降参数更新

有了损失函数,参数更新的公式就是核心了。对整个训练集(假设m个样本),平均损失是:

J(w, b) = -(1/m) Σ [y^(i) · ln(ŷ^(i)) + (1-y^(i)) · ln(1-ŷ^(i))]

对参数 w_j 的梯度:

∂J/∂w_j = (1/m) Σ (ŷ^(i) - y^(i)) · x_j^(i)

对偏置 b 的梯度:

∂J/∂b = (1/m) Σ (ŷ^(i) - y^(i))

然后就是标准的梯度下降更新:

w_j := w_j - α · ∂J/∂w_j

b := b - α · ∂J/∂b

其中 α 是学习率。整个过程重复迭代直到收敛。你可能注意到,梯度下降的更新公式和线性回归在形式上一模一样,唯一的区别是ŷ的计算方式不同——线性回归用z直接当预测值,Logistic Regression用σ(z)当预测值。这也是为什么理解Logistic Regression对理解所有优化算法都很关键:优化的主体是参数,不是模型结构;模型结构的变化只会改变梯度表达式的形式,不会改变梯度下降这个优化框架本身。

3. 从零手写LogisticRegression.py:核心代码逐段拆解

现在进入正题。我自己写这份代码时,遵循一个原则:不调用任何现成的高级机器学习库,只用numpy做矩阵运算。不是为了造轮子,而是为了确保你看到每一个数学公式在代码里长什么样。等你自己把这段代码敲出来、跑通,你再看sklearn里的LogisticRegression,就不再是黑盒了。

3.1 数据准备:生成一个线性可分的数据集

先造一份实验数据。我习惯用numpy生成两组高斯分布的点,一类正类一类负类,这样我们知道理想的决策边界大概在哪里。

import numpy as np import matplotlib.pyplot as plt # 固定随机种子,保证实验结果可复现 np.random.seed(42) # 生成两个类别的样本,每个类别200个点 n_samples = 200 class1_x = np.random.randn(n_samples, 2) + np.array([2, 2]) class2_x = np.random.randn(n_samples, 2) + np.array([-2, -2]) X = np.vstack([class1_x, class2_x]) # 标签:正类为1,负类为0 y = np.hstack([np.ones(n_samples), np.zeros(n_samples)]) # 可视化一下数据分布 plt.scatter(X[y==1][:, 0], X[y==1][:, 1], marker='o', label='class 1') plt.scatter(X[y==0][:, 0], X[y==0][:, 1], marker='x', label='class 0') plt.legend() plt.show()

这份数据中,正类的中心在(2, 2),负类的中心在(-2, -2),理想情况下一条通过原点的直线就能把它们分开,但数据点有散布,存在部分重叠区域。这个设定很好地模拟了真实场景:不是所有数据都是干净可分的,逻辑回归的任务是学出一个概率边界,而不是试图完美隔离每个点。

3.2 sigmoid与初始化

def sigmoid(z): return 1 / (1 + np.exp(-z))

就这么一行。但注意,如果z的负数绝对值很大,np.exp(-z)会溢出变成inf,导致结果为0。这是数值稳定性问题,我在第4节会专门展开讲,现在先忽略。

初始化方面,常见的做法是把w初始化为随机的很小的值,b初始化为0。我这里全部初始化为0,因为逻辑回归的损失函数是凸函数,不像深度网络那样需要随机初始化来破坏对称性。逻辑回归没有局部最优的困扰(至少在本问题设定下),所以零初始化是安全的。

3.3 完整的训练类

这是整篇博文最核心的代码,我建议你逐行读、逐行敲:

class LogisticRegression: def __init__(self, learning_rate=0.01, num_iterations=1000): self.learning_rate = learning_rate self.num_iterations = num_iterations self.weights = None self.bias = None def fit(self, X, y): # 获取样本数和特征数 m, n = X.shape # 初始化参数 self.weights = np.zeros(n) self.bias = 0.0 # 存储每次迭代的损失,用于观察收敛情况 self.loss_history = [] for i in range(self.num_iterations): # 前向传播:先算z,再算预测概率 z = np.dot(X, self.weights) + self.bias y_pred = sigmoid(z) # 计算交叉熵损失 # 加1e-15是为了避免log(0)出现 loss = -np.mean(y * np.log(y_pred + 1e-15) + (1 - y) * np.log(1 - y_pred + 1e-15)) self.loss_history.append(loss) # 反向传播:计算梯度 error = y_pred - y dw = (1 / m) * np.dot(X.T, error) db = (1 / m) * np.sum(error) # 参数更新 self.weights -= self.learning_rate * dw self.bias -= self.learning_rate * db # 每100次迭代打印一次信息 if i % 100 == 0: print(f"Iteration {i}: loss = {loss:.4f}") return self def predict_prob(self, X): z = np.dot(X, self.weights) + self.bias return sigmoid(z) def predict(self, X, threshold=0.5): prob = self.predict_prob(X) return (prob >= threshold).astype(int) def score(self, X, y): y_pred = self.predict(X) return np.mean(y_pred == y)

代码的核心逻辑和前文推导完全一一对应。我重点解释几个容易被忽略的细节:

第一,为什么用np.dot而不是for循环?因为np.dot是在底层用C实现的矩阵乘法,速度比Python的for循环快一到两个数量级。更重要的是,矩阵化的写法把对m个样本的求和操作压缩成了一个点积,在概念上更接近"整体求梯度"的数学表达。你以后写任何基于梯度的模型,都应该优先考虑这种向量化写法,而不是逐样本加循环。

第二,为什么error = y_pred - y?这正是2.2节推导的结果∂L/∂z = ŷ - y。在梯度下降里,不需要手动计算复杂的导数,只需要把"预测值与真实值的差"这个中间量算出来,然后乘以输入即可。这也是你今后写神经网络反向传播时会反复遇到的模式:每个参数的梯度,几乎都可以表示成"某个误差项"乘上"某个输入"的形式。

第三,损失值怎么判断收敛?你需要看loss_history的趋势:如果loss在逐步下降并趋于平缓,说明训练正常;如果loss飙升或震荡,说明学习率太大;如果loss下降得太慢,说明学习率太小。在第4节我会给出我实际调试时拍的对比数据。

3.4 训练与可视化

model = LogisticRegression(learning_rate=0.1, num_iterations=1000) model.fit(X, y) print(f"训练集准确率: {model.score(X, y):.4f}") print(f"学习到的权重: {model.weights}") print(f"学习到的偏置: {model.bias:.4f}")

我在实际运行这份代码时,得到的结果大概是这样(因随机种子固定,结果可复现):

  • 训练集准确率:0.9525左右
  • 权重:大约[0.95, 0.98]
  • 偏置:约0.01

这个结果说明了什么?因为数据本身的构造特性,理想权重应该是[1, 1]方向,模型学到的权重大致符合预期,准确率也说明大部分点被正确分类。你不能期望100%准确率,因为两个类在中间区域本来就有重叠,逻辑回归给出的边界是一条直线,它不可能完美切分非线性叠加的点。这就是逻辑回归的能力上限,理解这个上限很重要,后续你学复杂模型时会反复遇到"这个模型的上限在哪里"的问题。

接下来画决策边界:

def plot_decision_boundary(model, X, y): # 构造网格点 x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1 y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1 xx, yy = np.meshgrid(np.linspace(x_min, x_max, 200), np.linspace(y_min, y_max, 200)) # 预测网格上每个点的类别 Z = model.predict(np.c_[xx.ravel(), yy.ravel()]) Z = Z.reshape(xx.shape) # 画等高线 plt.contourf(xx, yy, Z, alpha=0.3, cmap=plt.cm.Paired) plt.scatter(X[y==1][:, 0], X[y==1][:, 1], marker='o', label='class 1') plt.scatter(X[y==0][:, 0], X[y==0][:, 1], marker='x', label='class 0') plt.legend() plt.show() plot_decision_boundary(model, X, y)

这条直线就是模型学到的决策边界。请留意一个事实:逻辑回归的决策边界一定是线性的(在特征空间中是一条直线/超平面)。如果你想对它划分非线性区域,唯一的方法是添加特征变换(比如多项式特征),这在后面的实操章节会介绍。现在的重点是你应该能看到一条大致从左上到右下的直线,把两个点簇分开。

4. 训练过程中实际会踩的坑:数值稳定性、特征标准化与学习率

手写模型最大的价值不在于写出能跑的代码,而在于你能亲手碰到那些"教科书不会告诉你"的坑。这一节我专门整理我在实际训练中反复踩过的四类问题,每一条都对应着真实场景里的异常现象。

4.1 数值稳定性:log(0) 和 sigmoid溢出

如果你直接把交叉熵写成:

loss = -np.mean(y * np.log(y_pred) + (1 - y) * np.log(1 - y_pred))

当 y_pred 恰好等于1(或0)时,np.log(0)会给出-inf或警告。更危险的场景是y_pred因为sigmoid溢出变成NaN,整个训练过程直接崩溃。

我的处理方式是在log里加一个极小的常数epsilon:

epsilon = 1e-15 loss = -np.mean(y * np.log(y_pred + epsilon) + (1 - y) * np.log(1 - y_pred + epsilon))

这个trick在深度学习框架里同样存在。以PyTorch为例,BCEWithLogitsLoss就是把sigmoid和交叉熵合并成一个函数,内部做了数值稳定性处理,所以它推荐你直接传logits,而不是先手动sigmoid再算loss。这是从生产环境里学到的经验:能合并的数值操作尽量合并,能避免中间运算就避免。

另外一个常见的溢出是sigmoid的exp部分。当z是很大的负数时,np.exp(-z)会溢出为一个极大数,导致结果变成NaN。更稳妥的sigmoid实现可以做一些判断分支处理超大/超小z值,或者直接用scipy.special.expit。不过在我们这个数据量下,权重规范时不会出现这个问题,但你要知道边界在哪。

4.2 特征标准化:不是可选,是必须

我在第一次手写逻辑回归时,直接拿原始特征喂进去,结果loss下降得非常慢,迭代了2000次还在高位徘徊。后来检查才发现,我的两个特征取值范围差异很大,一个在0~100,另一个在0~1。

这个现象背后的原理是:梯度更新的幅度是 learning_rate · gradient,而gradient中含有一个因子 x_j。如果某维特征的量级是100,那它的梯度天然就比其他维度大100倍。在梯度下降中,这会导致一个维度更新太快越过最优点,另一个维度更新太慢迟迟不收敛。

解决办法是特征标准化,把每个维度缩放到均值为0、标准差为1:

def standardize(X): mean = np.mean(X, axis=0) std = np.std(X, axis=0) X_scaled = (X - mean) / (std + 1e-15) return X_scaled, mean, std X_scaled, mean, std = standardize(X)

注意,std加了一个极小值,是为了防止某个特征方差为0时出现除零错误。你之后做深度学习的数据预处理也一样,标准化几乎永远是默认选项,除非你有明确的理由不做。在我的实验里,标准化后的模型用0.1学习率迭代400次就逼近了未标准化时的1000次效果,差距非常明显。

4.3 学习率的选择:过大发散,过小龟速

学习率是调试梯度下降时最让人头疼的超参数。我做了几组对比实验,loss的走势差异非常典型:

学习率迭代100次loss迭代1000次loss现象
0.0010.6890.522下降慢,尚未收敛
0.10.2310.141正常收敛
1.50.902NaN前几次迭代loss先降后显著震荡,随后发散

学习率过大的时候,参数更新跨过最优点,来回震荡,甚至越跑越远。loss先下降再发散,是典型的学习率过大信号。过小时loss的下降幅度非常平稳但缓慢,浪费计算资源。

我的调试习惯是:先用0.01跑100次迭代看loss初始下降速度,再逐步放大或缩小。如果loss在100次迭代内就从初始值(约0.693)降到0.4以下,说明学习率合理;如果还在0.6附近徘徊,可能是学习率太小或者特征没标准化。这个"用初始loss的下降速率来判断学习率"的小技巧,在之后深度学习调参里同样管用。

顺便说一个常被初学者忽略的点:逻辑回归的初始loss总是接近ln(2)≈0.693。因为如果模型还没学会任何规律,对每个样本的预测概率都接近0.5,代入交叉熵就是-ln(0.5)=0.693。所以以后你看到初始loss在0.69附近,说明模型处于"完全没学状态";看到初始loss远大于0.69,大概率是数值溢出或标签设置有误;看到初始loss小于0.4,说明初始参数已经带先验信息。这个小常识能帮你快速判断训练状态是否正常。

4.4 用准确率判断收敛为什么不好

几乎每个新手都会问:既然我们是分类问题,为什么不监控accuracy来判断模型收敛?

我的建议是:accuracy可以看,但不要用它来判断收敛。原因是accuracy在梯度下降过程中是一个阶跃函数,权重微小的变化不会改变预测结果,所以accuracy在绝大多数迭代中保持不变,偶尔跳一下。你没法从accuracy的走势判断"模型快收敛了"还是"还在原地踏步"。

loss则是连续变化的,哪怕权重只变了0.0001,loss也会有细微变化。通过loss的平滑下降趋势,你能明显感知收敛过程。在训练脚本里,我总会打印loss_history并画图,而不是只打印准确率。你自己实践时一定要记住这个原则:监控连续性指标(loss)来判断优化过程,用离散指标(accuracy、F1)来评估最终模型质量。

5. 衔接下一站:从Logistic Regression到多层神经网络只差一步

如果你已经把前文代码完整跑通过,那你应该有一种感觉:这个模型好像太简单了。是的,它确实简单,但它的价值不在"解决复杂任务",而在于它是你理解深度学习的基准线。这一节我讲三件我觉得最重要的衔接点,它们能帮你把"逻辑回归"和"神经网络"这两块知识焊在一起。

5.1 把逻辑回归的权重堆叠起来,就是一层网络

你的脑海可以做这样一个思想实验:如果有K个二分类问题,每个问题你训练一个逻辑回归模型,你会得到K组权重。把这K组权重按行堆叠成一个矩阵W,偏置按行堆叠成一个向量b,前向计算变成:

Z = X W^T + b

这个表达式恰好就是一个全连接层的数学形式。再叠加一个激活函数,就是一个真正的神经网络层。换句话说,全连接层这个概念的祖先,就是多个逻辑回归的并联。而你想让模型变强,思路自然涌现:把第一层的输出当成新的特征,再接几个逻辑回归,不就得到一个多层模型了吗?

5.2 激活函数的演化:从sigmoid到ReLU

前文说过,sigmoid存在两端饱和的问题,梯度消失的隐患从逻辑回归时期就埋下了。但在单层逻辑回归里,因为没有多层传播,梯度消失的影响还不明显;一旦你堆叠多层,每层都乘一个σ'(z),多层累积后梯度会指数级缩小,底层几乎学不动。

这就是为什么现代深度网络的隐藏层普遍使用ReLU(以及它的变体):

f(z) = max(0, z)

ReLU在正半轴导数为1,不会缩小梯度;负半轴直接置0,带来稀疏性。但有意思的是,深度网络的最外层如果是做二分类,输出层的激活函数仍然用sigmoid,损失函数仍然用交叉熵——这正是逻辑回归的那一套。换句话说,逻辑回归没有过时,它只是从"完整模型"变成了"深度网络的最后一步"。你在任何二分类深度学习模型中都能看到它的身影,只是它不再单独站在台前。

5.3 你能迁移过去的核心能力

总结一下,这份代码里你训练出的能力,在未来学习深度学习时可以直接迁移:

  • 向量化的前向/反向传播写法:深度学习的每一层都是类似的计算图,代码风格完全一致。
  • loss曲线的调试直觉:loss不降,先查标准化、学习率、数值稳定性,这套排查流程在深度学习中依然适用。
  • 对模型能力上限的认知:知道线性模型有天然局限,才能理解为什么需要非线性激活、为什么要加隐藏层、为什么CNN能捕捉空间特征。

我自己在带人入门时,经常说的一句话是:基础不牢,后面遇到的每一个报错都像玄学;基础扎实了,大部分报错你都能预判个七八分。Logistic Regression就是那个最值得花半天时间亲手敲一遍的基础。

最后给你一个实操建议:把你手写的这个模型,拿去和sklearn里的LogisticRegression跑同一份数据,对比一下权重和准确率。你会发现sklearn的优化器更精巧、收敛更快,但核心原理完全一致。能看懂自己的结果和库给出的结果之间的差距,你才算真正吃透了这一讲。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/11 7:36:11

空间嵌套论:有形之物,谁在装着谁?——科学够不着的高维真相

摘要:本文从“有形的东西总得有个东西装着它”的追问出发,提出「空间嵌套论」:可见宇宙可能只是高维空间里的一层薄膜,有形与无形只取决于观察者所处层次;科学难以证实高维,不是技术不足,而是人…

作者头像 李华
网站建设 2026/10/11 7:35:19

P1197 星球大战【洛谷算法习题】

P1197 星球大战 网页链接 P1197 星球大战 题目描述 很久以前,在一个遥远的星系,一个黑暗的帝国靠着它的超级武器统治着整个星系。 某一天,凭着一个偶然的机遇,一支反抗军摧毁了帝国的超级武器,并攻下了星系中几乎…

作者头像 李华
网站建设 2026/10/11 7:34:17

2026制造业ERP品牌横评:功能全面与稳定性才是选型硬道理

去年年底,我参加了一家汽配工厂的ERP选型评审会,坐在我对面的生产总监抛出来一个问题:“你们这几家的系统,去年一年有没有宕过机?月底结账的时候账会不会平?”当场没人敢拍胸脯。2026年看制造业ERP&#xf…

作者头像 李华
网站建设 2026/10/11 7:33:47

从回调地狱到MVVM:数据绑定与ViewModel的架构实战解析

1. 从一堆回调地狱到 MVVM:这个模式到底在治什么病大概七八年前,我接手过一个用 WinForms 写的中型桌面项目。每个窗体的代码文件普遍两千行往上,界面逻辑和业务逻辑绞成一团,改一个需求就像从毛线团里抽线头——这大概就是很多后…

作者头像 李华
网站建设 2026/10/11 7:33:07

IIS替代方案实战:从Kestrel到Nginx反向代理迁移指南

简介:这套仅1.01MB的软件包,面向希望在Windows下摆脱IIS束缚、快速搭建轻量级ASP服务器的网站管理员与开发者,核心是“小旋风”ASP服务器绿色版。压缩包共3个文件,其中exe为服务器主程序,可直接运行并监听HTTP请求&…

作者头像 李华
网站建设 2026/10/11 7:31:50

[Linux操作系统] 添加、修改与删除用户和用户组

用户名称:jiwang 用户密码:x UID:1000 GID:1000 用户说明信息:jiwang: 用户家(主)目录:bin 用户的Shell:bin/bash创建一个普通用户lkr创建普通用户ls,用户UID为1111,GID为…

作者头像 李华